自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

SwiftUI 布局協(xié)議 - Part2

移動開發(fā) iOS
深入布局協(xié)議讓我對 HStack 或 VStack 等容器編寫代碼的團(tuán)隊(duì)有了新的認(rèn)識。

前言

在 Part 1 我們探索了布局協(xié)議的基礎(chǔ)知識,為理解布局是如何工作的打下了堅(jiān)實(shí)的基礎(chǔ)?,F(xiàn)在,是時候深入研究那些更少提及的功能了,以及如何使用它們來為我們帶來便利。

Part 1 - 基礎(chǔ):

  • 什么是布局協(xié)議
  • 視圖層次結(jié)構(gòu)的族動態(tài)
  • 我們的第一個布局實(shí)現(xiàn)
  • 容器對齊
  • 自定義值:LayoutValueKey
  • 默認(rèn)間距
  • 布局屬性和Spacer()
  • 布局緩存
  • 高明的偽裝者
  • 使用AnyLayout 切換布局
  • 結(jié)語

Part 2 - 高級布局:

  • 前言
  • 自定義動畫
  • 雙向自定義值
  • 避免布局循環(huán)和崩潰
  • 遞歸布局
  • 布局組合
  • 插入兩個布局
  • 使用綁定參數(shù)
  • 一個有用的調(diào)試工具
  • 最后的思考

自定義動畫

讓我們從寫一個圓形布局的視圖容器開始吧。我們將它叫做 WheelLayout:

圖片

struct ContentView: View {
let colors: [Color] = [.yellow, .orange, .red, .pink, .purple, .blue, .cyan, .green]

var body: some View {
WheelLayout(radius: 130.0, rotation: .zero) {
ForEach(0..<8) { idx in
RoundedRectangle(cornerRadius: 8)
.fill(colors[idx%colors.count].opacity(0.7))
.frame(width: 70, height: 70)
.overlay { Text("\(idx+1)") }
}
}
}
}
struct WheelLayout: Layout {
var radius: CGFloat
var rotation: Angle
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let maxSize = subviews.map { $0.sizeThatFits(proposal) }.reduce(CGSize.zero) {
return CGSize(width: max($0.width, $1.width), height: max($0.height, $1.height))
}
return CGSize(width: (maxSize.width / 2 + radius) * 2,
height: (maxSize.height / 2 + radius) * 2)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ())
{
let angleStep = (Angle.degrees(360).radians / Double(subviews.count))
for (index, subview) in subviews.enumerated() {
let angle = angleStep * CGFloat(index) + rotation.radians
// Find a vector with an appropriate size and rotation.
var point = CGPoint(x: 0, y: -radius).applying(CGAffineTransform(rotationAngle: angle))
// Shift the vector to the middle of the region.
point.x += bounds.midX
point.y += bounds.midY
// Place the subview.
subview.place(at: point, anchor: .center, proposal: .unspecified)
}
}
}

當(dāng)布局發(fā)生改變時,SwfitUI 提供了內(nèi)置動畫支持。所以如果我們將輪子的旋轉(zhuǎn)值更改為90度,我們將會看見它是如何逐漸的移動到新的位置上:

圖片

WheelLayout(radius: radius, rotation: angle) {
// ...
}
Button("Rotate") {
withAnimation(.easeInOut(duration: 2.0)) {
angle = (angle == .zero ? .degrees(90) : .zero)
}
}

這非常好我本可以在此結(jié)束動畫部分。但是,你已經(jīng)知道我們的博客不滿足于膚淺的表面,所以讓我們深層次的看看到底發(fā)生了什么。

當(dāng)我們改變角度時,SwiftUI 會計(jì)算好每個視圖最初和最終的位置,然后在動畫期間內(nèi)修改它們的位置,從A點(diǎn)到B點(diǎn)成一條直線。起初它似乎沒有這樣做,但是檢查下面這個動畫,集中注意觀察單個視圖,看看它們是如何都跟隨直虛線移動的?

圖片

你有想過如果動畫的角度是從0到360會發(fā)生什么嗎?給你一分鐘... 對!...什么都不會發(fā)生。開始的位置和結(jié)束的位置是一樣的,因此就 SwiftUI 而言,沒有動畫。

如果這就是你要找的東西,那就太好了,但由于我們將視圖圍繞一個圓圈放置,如果視圖沿著那個假想的圓圈移動不是更有意義嗎?好吧,事實(shí)證明,這樣做非常容易!

我們問題的答案是很幸運(yùn)的,這個布局協(xié)議采用 Animatable 協(xié)議!如果你不知道或者忘記這是什么,我建議你查看 SwiftUI 布局協(xié)議 - Part 1 的 Animating Shape Paths 部分 。

簡單的說,通過添加 animatableData 屬性到我們的布局,我們要求 SwiftUI 動畫的每一幀重新計(jì)算布局。但是,在每個布局傳遞中,角度都會收到一個內(nèi)插值?,F(xiàn)在 SwiftUI 不會為我們插入位置。相反,它會插入角度值。我們的布局代碼將會完成剩下的工作。

圖片

struct Wheel: Layout {
// ...
var animatableData: CGFloat {
get { rotation.radians }
set { rotation = .radians(newValue) }
}
// ...
}

添加一個 animatableData 屬性足夠讓我們的視圖正確的跟隨圓圈。但是,既然我們已經(jīng)到了這步。。。為什么我們不讓半徑也可以動畫呢?

var animatableData: AnimatablePair<CGFloat, CGFloat> {
get { AnimatablePair(rotation.radians, radius) }
set {
rotation = Angle.radians(newValue.first)
radius = newValue.second
}
}

雙向自定義值

在文章的第一部分我們了解到如何使用 LayoutValues 將信息附加到視圖,以便它們的代理可以在 placeSubviews 和 sizeThatFits 方法中暴露這些信息。我們的想法是信息從視圖流向布局,一會兒將看見這一點(diǎn)是如何被逆轉(zhuǎn)。

本節(jié)所解釋的想法應(yīng)謹(jǐn)慎使用,以避免布局循環(huán)和  CPU 峰值。在下一部分我將會解釋原因和如何避免它。但是不用擔(dān)心,這并不復(fù)雜,你只需要遵循一些準(zhǔn)則。

讓我們回到輪子的這個例子,假設(shè)我們想要視圖旋轉(zhuǎn)起來,讓它們指向中心。

圖片

布局協(xié)議只能決定視圖位置和它們的建議尺寸,但是不能應(yīng)用樣式、旋轉(zhuǎn)或者其他的效果。如果我們想要這些效果,那么布局應(yīng)該有一種傳達(dá)回視圖的方式。這時候布局值就變得重要起來,到目前為止,我們已經(jīng)使用它們傳遞信息給布局,但只要加上一點(diǎn)創(chuàng)意,我們就可以反向使用它們。

我之前提到過的 LayoutValues 并不局限于傳遞 ??CGFloats?? ,你可以將它用于任何事情,包括??Binding??,在這個例子中,我們將使用 ??Binding<Angle>??:

struct Rotation: LayoutValueKey {
static let defaultValue: Binding<Angle>? = nil
}

注意:我稱它為雙向自定義值,因?yàn)樾畔⑹强梢噪p向流動的,但是,這不是 SwiftUI 的官方術(shù)語,只是為了更清晰的解釋這個想法的術(shù)語。

在布局的 placeSubview 方法中,我們設(shè)置每個子視圖的角度:

struct WheelLayout: Layout {
// ...
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ())
{
let angleStep = (Angle.degrees(360).radians / Double(subviews.count))
for (index, subview) in subviews.enumerated() {
let angle = angleStep * CGFloat(index) + rotation.radians
// ...
DispatchQueue.main.async {
subview[Rotation.self]?.wrappedValue = .radians(angle)
}
}
}
}

回到我們的視圖,我們可以讀取值,并用它來旋轉(zhuǎn)視圖:

struct ContentView: View {
// ...
@State var rotations: [Angle] = Array<Angle>(repeating: .zero, count: 16)
var body: some View {
WheelLayout(radius: radius, rotation: angle) {
ForEach(0..<16) { idx in
RoundedRectangle(cornerRadius: 8)
.fill(colors[idx%colors.count].opacity(0.7))
.frame(width: 70, height: 70)
.overlay { Text("\(idx+1)") }
.rotationEffect(rotations[idx])
.layoutValue(key: Rotation.self, value: $rotations[idx])
}
}
// ...
}

這段代碼將確保所有的視圖都指向圓心,但是我們可以更優(yōu)雅一點(diǎn)。我提供的解決方案需要設(shè)置一個旋轉(zhuǎn)數(shù)組,將它們作為布局值然后使用這些值旋轉(zhuǎn)視圖。如果我們可以向布局用戶隱藏這種復(fù)雜性那不是很好嗎?這里就是重寫之后的。

首先我們創(chuàng)建一個封裝視圖 WheelComponent:

struct WheelComponent<V: View>: View {
@ViewBuilder let content: () -> V
@State private var rotation: Angle = .zero
var body: some View {
content()
.rotationEffect(rotation)
.layoutValue(key: Rotation.self, value: $rotation)
}
}

然后我們擺脫旋轉(zhuǎn)數(shù)組(我們不再需要了!)并將每個視圖包裝在 WheelComponent視圖中。

WheelLayout(radius: radius, rotation: angle) {
ForEach(0..<16) { idx in
WheelComponent {
RoundedRectangle(cornerRadius: 8)
.fill(colors[idx%colors.count].opacity(0.7))
.frame(width: 70, height: 70)
.overlay { Text("\(idx+1)") }
}

}
}

就是這樣。用戶使用容器只需要記住將視圖封裝在 WheelComponent里面。他們不需要擔(dān)心布局值,綁定,角度等等。當(dāng)然,不在封裝里的視圖不會受到任何影響,視圖不會旋轉(zhuǎn)指向中心。

我們還可以添加一個改進(jìn),那就是視圖旋轉(zhuǎn)的動畫。仔細(xì)觀察并比較下面三個輪子:一個不旋轉(zhuǎn)。另外兩個旋轉(zhuǎn)指向中心,但是一個不使用動畫而另一個使用。

避免布局循環(huán)和崩潰

眾所周知我們在布局期間不能更新視圖狀態(tài)。這會導(dǎo)致不可預(yù)測的結(jié)果,很可能會使 CPU 達(dá)到峰值。在此之前我們看到過這種情況,即閉包在布局期間運(yùn)行時,也許當(dāng)時不是太明顯。但是現(xiàn)在,這是毫無疑問的。sizeThatFits 和 placeSubviews 是布局過程中的一部分。因此當(dāng)我們使用上一部分中描述的"欺騙"的技巧,我們必須使用 DispatchQueue 用隊(duì)列更新。就像上面的例子一樣:

DispatchQueue.main.async {
subview[Rotation.self]?.wrappedValue = .radians(angle)
}

使用雙向自定義值還有另一個潛在的問題,那就是你的視圖必須在不影響別的布局的前提下使用該值,否則的話你將會陷入布局循環(huán)。

例如,如果用 placeSubviews 設(shè)置去更改視圖顏色,那就是安全的。在案例中,可能看起來旋轉(zhuǎn)會影響布局,但其實(shí)不是這樣的,當(dāng)你旋轉(zhuǎn)視圖,它的周圍從來沒產(chǎn)生影響,邊界仍然保持不變。如果你設(shè)置了偏移,或者其他的變換矩陣,也會發(fā)生同樣的事情。但無論如何,我建議你監(jiān)測 CPU 來發(fā)現(xiàn)布局中其他潛在的問題。如果 CPU 開始飆升,或許可以在 placeSubviews 中添加一條打印語句查看它是否無休止的調(diào)用。注意動畫也會使 CPU 增長。如果你想測試你的容器是否循環(huán),不要在動畫時查看 CPU 。

注意這不是新問題。過去我們在使用 GeometryReader? 獲取視圖尺寸并傳遞值到父視圖的時候遇到過這個問題,然后父視圖使用該信息去改變視圖,即使用 GeometryReader? 去再一次改變,然后我們就陷入了布局循環(huán)。這是個老問題,我在 SwiftUI 剛發(fā)布的時候就寫過此類問題,在 Safely Updating The View State [1] 一文中可以查看更多信息。

我還想再提一下潛在的崩潰。這與雙向自定義值無關(guān)。這是你在寫任何布局都必須要考慮的。我們提到 SwiftUI? 可能會多次調(diào)用 sizeThatFits 去測試視圖的靈活性。在這些調(diào)用中,你返回的值應(yīng)該是合理的。例如,下面的代碼會崩潰:

struct ContentView: View {
var body: some View {
CrashLayout {
Text("Hello, World!")
}
}
}
struct CrashLayout: Layout {
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
if proposal.width == 0 {
return CGSize(width: CGFloat.infinity, height: CGFloat.infinity)
} else if proposal.width == .infinity {
return CGSize(width: 0, height: 0)
}

return CGSize(width: 0, height: 0)
}

func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ())
{
}
}

這里 sizeThatFits 返回了 .infinity 作為最小尺寸, .zero 作為最大尺寸。這是不合理的,最小尺寸不可能大于最大尺寸!

遞歸布局

在下面這個例子中我們將探索遞歸布局。我們將會把之前的 WheelLayout 視圖轉(zhuǎn)變?yōu)?nbsp;RecursiveWheel.我們的新布局將會在圓圈里放置12個視圖。里面的12個視圖將會按比例縮小到內(nèi)圈中,直到它們不會再有別的視圖。視圖的縮放和旋轉(zhuǎn)要再一次使用雙向自定義值實(shí)現(xiàn)。

在這個例子中在容器中一共有44個視圖,所以我們的新容器將會分別以12,12,12和8為一圈。

注意本案例中如何使用緩存與子視圖通信。這是可以實(shí)現(xiàn)的,因?yàn)榫彺媸且粋€ inout 參數(shù),我們可以在 placeSubviews 中更新。

placeSubviews 方法遍歷并放置12個子視圖:

for (index, subview) in subviews[0..<12].enumerated() {
// ...
}

然后遞歸調(diào)用placeSubviews 但僅限于剩下的視圖,如此直到?jīng)]有別的視圖。

placeSubviews(in: bounds,
proposal: proposal,
subviews: subviews[12..<subviews.count],
cache: &cache)

布局組合

在上一個例子中我們使用了相同的布局遞歸。但是,我們也可以組合一些不同布局到容器中。在下一個例子中我們將會把前三個視圖水平的放置在視圖頂部,后三個水平的放置在底部。剩下的視圖將會在中間,垂直排列。

圖片

我們不需要編寫垂直或者水平的間距邏輯代碼,因?yàn)?nbsp;SwiftUI 已經(jīng)有這樣的布局了:HStackLayout 和 VStackLayout。

只是有點(diǎn)小問題,很輕易就可以修復(fù)。由于某些原因,系統(tǒng)布局私下實(shí)現(xiàn)了 sizeThatFits 和 placeSubviews 。這意味著我們無法調(diào)用它們。但是,類型消除布局確實(shí)暴露它的所有方法,所以不要這樣做:

HStackLayout(spacing: 0).sizeThatFits(...) // not possible

我們可以:

AnyLayout(HStackLayout(spacing: 0)).sizeThatFits(...) // it is possible!

此外,在與其他視圖布局工作的時候,我們就相當(dāng)于 SwiftUI 的角色。子布局的任何緩存創(chuàng)建和更新都屬于我們的責(zé)任,幸運(yùn)的是,這都很容易處理。我們只需要添加子布局緩存到我們自己的緩存里。

struct ComposedLayout: Layout {
private let hStack = AnyLayout(HStackLayout(spacing: 0))
private let vStack = AnyLayout(VStackLayout(spacing: 0))

struct Caches {
var topCache: AnyLayout.Cache
var centerCache: AnyLayout.Cache
var bottomCache: AnyLayout.Cache
}

func makeCache(subviews: Subviews) -> Caches {
Caches(topCache: hStack.makeCache(subviews: topViews(subviews: subviews)),
centerCache: vStack.makeCache(subviews: centerViews(subviews: subviews)),
bottomCache: hStack.makeCache(subviews: bottomViews(subviews: subviews)))
}

// ...
}

插入兩個布局

另一個組合案例:插入兩個布局

下一個例子將會創(chuàng)建一個以輪子,或者波浪形式顯示視圖的布局。當(dāng)然它還提供了一個從 0.0 到1.0 的 pct 參數(shù),當(dāng) pct == 0.0,視圖將會展示輪子,當(dāng)pct == 1.0,視圖將會展示 sin波動。中間的數(shù)值將會穿插在兩者的位置之中。

在我們創(chuàng)建組合布局之前,讓我先來介紹一下 WaveLayout,這個布局有好幾個參數(shù)來讓你改變正弦波動的幅度,頻率和角度。

InterpolatedLayout 將會計(jì)算兩個布局(波動和輪子)的尺寸和位置然后它將插入這些值以進(jìn)行最終定位。注意。在 placeSubviews 方法中,如果子視圖被多次定位,最后一次調(diào)用place() 

使用以下公式計(jì)算插值:

(wheelValue * pct) + (waveValue * (1-pct))

我們需要一種方法讓  WaveLayout 和 WheelLayout 將每一個視圖位置和旋轉(zhuǎn)返回給 InterpolatedLayout 。我們通過緩存做到了這一點(diǎn)。我們再一次看到了緩存不是性能提升的唯一用途。

我們也需要  WaveLayout 和 WheelLayout 去檢測它們是否被 InterpolatedLayout 使用,以便它們可以相應(yīng)的更新緩存。這些視圖可以輕易的檢測到這種情況,這要?dú)w功于獨(dú)立的緩存值,如果緩存是由 InterpolatedLayout  創(chuàng)建的,則該值僅為 false。

使用綁定參數(shù)

今年 SwfitUI Lounges 出現(xiàn)了一個有趣的問題,詢問是否可能使用新的布局協(xié)議去創(chuàng)建一個層次樹,用線連接。挑戰(zhàn)的不是視圖樹結(jié)構(gòu),而是我們?nèi)绾萎嬤B接線。

圖片

還有其它方法可以實(shí)現(xiàn)它,例如,使用 Canvas[2] ,但是我們這里都是關(guān)于布局協(xié)議的,讓我們來看看可以如何解決連接線的問題。

我們現(xiàn)在都知道,這根線不可能被布局繪制出來。那我們需要的是一種讓布局告訴視圖如何繪制線條的方法。初步想法可以(在這個問題上蘋果的工程師是這么建議的[3]) 使用布局值。這正是我們在上一個例子中做的事情,雙向自定義值。但是,仔細(xì)思考之后,還有一種更簡單的方式。

相比于使用布局值去分別通知樹的每個節(jié)點(diǎn)的最終位置,使用布局代碼創(chuàng)建整個路徑來的更簡單一點(diǎn)。然后,我們只需要將路徑返回給負(fù)責(zé)展示的視圖。通過添加綁定布局參數(shù)很容易完成。

struct TreeLayout {

@Binding var linesPath: Path

// ...
}

在完成放置視圖以后,我們知道了位置并使用它們的坐標(biāo)去創(chuàng)建路徑。再次注意,我們必須非常小心的避免布局循環(huán)。我發(fā)現(xiàn)更新路徑會產(chǎn)生一個循環(huán),即使該路徑被繪制為不影響布局的背景視圖也是如此,所以為了避免這種循環(huán),我們要確保路徑發(fā)生改變,然后才更新綁定,這樣就可以成功的打破循環(huán)。

let newPath = ...

if newPath.description != linesPath.description {

DispatchQueue.main.async {
linesPath = newPath
}

}

這個挑戰(zhàn)的另一個有趣的部分是告訴布局這些視圖如何分層連接。在本例中,我創(chuàng)建了兩個 UUID 布局值,一個標(biāo)識視圖,另一個作為父視圖的 ID。

struct ContentView: View {
@State var path: Path = Path()

var body: some View {
let dash = StrokeStyle(lineWidth: 2, dash: [3, 3], dashPhase: 0)

TreeLayout(linesPath: $path) {
ForEach(tree.flattenNodes) { node in
Text(node.name)
.padding(20)
.background {
RoundedRectangle(cornerRadius: 15)
.fill(node.color.gradient)
.shadow(radius: 3.0)
}
.node(node.id, parentId: node.parentId)
}
}
.background {
// Connecting lines
path.stroke(.gray, style: dash)
}
}
}

extension View {
func node(_ id: UUID, parentId: UUID?) -> some View {
self
.layoutValue(key: NodeId.self, value: id)
.layoutValue(key: ParentNodeId.self, value: parentId)
}
}

當(dāng)我們使用這些代碼的時候需要注意一些事項(xiàng)。這里應(yīng)該只有一個父節(jié)點(diǎn)是 nil 的節(jié)點(diǎn)(根結(jié)點(diǎn)),你應(yīng)該小心的避免循環(huán)引用(例如:兩個節(jié)點(diǎn)互為父節(jié)點(diǎn))。

同時也要注意,這里有一個好的選擇,即放置到具有垂直和水平的滾動 ScrollView 中。

注意這是基本實(shí)現(xiàn),僅用于說明如何實(shí)現(xiàn)。還有許多潛在的優(yōu)化,但制作樹布局所需的關(guān)鍵元素都在這里。

一個有用的調(diào)試工具

回到當(dāng) SwiftUI 剛發(fā)布的時候,我盡力搞清楚布局是如何工作的,我希望我有一個像我今天要介紹的這種工具 。直到現(xiàn)在,它都是最好的工具,用來添加圍繞視圖的邊框觀察視圖邊緣。那是我們最好的盟友。

使用邊框依然是很好的調(diào)試工具,但我們可以添加一個新的工具。感謝新的布局協(xié)議,我創(chuàng)建了一個修飾器,它在嘗試?yán)斫鉃槭裁匆晥D不像您認(rèn)為的那樣工作的時候非常有用,修飾器在這兒:

func showSizes(_ proposals: [MeasureLayout.SizeRequest] = [.minimum, .ideal, .maximum]) -> some View

你可以在任何視圖使用它,一個覆蓋層將會浮動在視圖的頭部角落,顯示一組給定的建議尺寸。如果你未制定建議,最小,理想和最大尺寸都將被覆蓋。

MyView()
.showSizes()

一些使用示例:

showSizes() // minimum, ideal and maximum
showSizes([.current, .ideal]) // the current size of the view and the ideal size
showSizes([.minimum, .maximum]) // the minimum and maximum
showSizes([.proposal(size: ProposedViewSize(width: 30, height: .infinity))]) // a specific proposal

更多的例子:

圖片

ScrollView {
Text("Hello world!")
}
.showSizes([.current, .maximum])

Rectangle()
.fill(.yellow)
.showSizes()

Text("Hello world")
.showSizes()
Image("clouds")
.showSizes()

Image("clouds")
.resizable()
.aspectRatio(contentMode: .fit)
.showSizes([.minimum, .ideal, .maximum, .current])

突然間一切都變得有意義了。例如:檢查一下使用和不使用 resizable()的圖像尺寸。終于能看到數(shù)字是不是有一種奇怪的滿足感?

總結(jié)

即使你不打算寫你自己的布局容器,明白它是如何工作也會幫助你理解布局在 SwiftUI 的一般工作方式。

就個人而言,深入布局協(xié)議讓我對 HStack 或  VStack 等容器編寫代碼的團(tuán)隊(duì)有了新的認(rèn)識。我經(jīng)常認(rèn)為這些視圖是理所當(dāng)然的,并將它們視為簡單而不復(fù)雜的容器,好吧,嘗試編寫自己的版本,在各種情況下復(fù)制一個 HStack ,多種類型的視圖和布局優(yōu)先級競爭同一個空間。。。這是一個不錯的挑戰(zhàn)!

參考資料

[1]Safely Updating The View State : https://swiftui-lab.com/state-changes/?。

[2]Canvas: https://swiftui-lab.com/swiftui-animations-part5/?。

[3]建議: http://swiftui-lab.com/digital-lounges-2022#layout-9?。

責(zé)任編輯:姜華 來源: Swift社區(qū)
相關(guān)推薦

2011-04-01 16:30:26

T-SQLDateTime

2022-12-07 09:01:14

布局容器VStack?

2017-03-30 07:56:30

測試前端代碼

2022-02-14 09:24:15

SwiftUI協(xié)議

2017-09-13 23:28:01

2020-11-13 10:06:47

XignCode3

2014-03-13 14:12:52

2021-01-13 09:42:58

站點(diǎn)隔離Chrome漏洞

2011-08-15 10:10:47

編程

2015-07-08 13:36:24

2020-09-15 06:15:23

滲透測試風(fēng)險評估網(wǎng)絡(luò)安全

2018-04-04 15:03:14

2013-11-19 11:59:49

Linux命令Shell腳本

2011-07-04 13:17:18

Qt Designer 布局

2021-05-20 09:00:27

SwiftUI Swift TapGesture

2010-06-11 16:12:00

RIP-V2協(xié)議

2010-06-28 11:15:45

BitTorrent協(xié)

2023-06-09 09:00:36

Swift視圖修飾符

2022-08-24 09:02:27

SwiftUIiOS
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號