From 611bab17a49523fa8f21dc37611046ac9cddd91c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=B4=E9=93=AD?= Date: Thu, 16 May 2024 23:50:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=B8=83=E5=B1=80=E5=86=85?= =?UTF-8?q?=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SwiftPamphletApp.xcodeproj/project.pbxproj | 40 ++++++++- .../Guide/View/GuideListView.swift | 8 ++ .../InfoOrganizer/Info/InfoRowView.swift | 5 +- .../Safe Area(ap).md" | 89 +++++++++++++++++++ .../AnyLayout(ap).md" | 42 +++++++++ .../GeometryReader(ap).md" | 71 +++++++++++++++ .../Layout\345\215\217\350\256\256(ap).md" | 42 +++++++++ .../ViewThatFits(ap).md" | 78 ++++++++++++++++ .../alignmentGuide(ap).md" | 34 +++++++ ...50\200\203\350\265\204\346\226\231(ap).md" | 18 ++++ .../Grid(ap).md" | 44 +++++++++ 11 files changed, 465 insertions(+), 6 deletions(-) create mode 100644 "SwiftPamphletApp/Resource/Guide/SwiftUI/\345\270\203\345\261\200\347\273\204\344\273\266/\345\270\203\345\261\200\350\277\233\351\230\266/AnyLayout(ap).md" create mode 100644 "SwiftPamphletApp/Resource/Guide/SwiftUI/\345\270\203\345\261\200\347\273\204\344\273\266/\345\270\203\345\261\200\350\277\233\351\230\266/GeometryReader(ap).md" create mode 100644 "SwiftPamphletApp/Resource/Guide/SwiftUI/\345\270\203\345\261\200\347\273\204\344\273\266/\345\270\203\345\261\200\350\277\233\351\230\266/Layout\345\215\217\350\256\256(ap).md" create mode 100644 "SwiftPamphletApp/Resource/Guide/SwiftUI/\345\270\203\345\261\200\347\273\204\344\273\266/\345\270\203\345\261\200\350\277\233\351\230\266/ViewThatFits(ap).md" create mode 100644 "SwiftPamphletApp/Resource/Guide/SwiftUI/\345\270\203\345\261\200\347\273\204\344\273\266/\345\270\203\345\261\200\350\277\233\351\230\266/alignmentGuide(ap).md" create mode 100644 "SwiftPamphletApp/Resource/Guide/SwiftUI/\345\270\203\345\261\200\347\273\204\344\273\266/\345\270\203\345\261\200\350\277\233\351\230\266/\345\270\203\345\261\200\350\277\233\351\230\266-\345\217\202\350\200\203\350\265\204\346\226\231(ap).md" diff --git a/SwiftPamphletApp.xcodeproj/project.pbxproj b/SwiftPamphletApp.xcodeproj/project.pbxproj index a493c6ce7..c00b5e6c8 100644 --- a/SwiftPamphletApp.xcodeproj/project.pbxproj +++ b/SwiftPamphletApp.xcodeproj/project.pbxproj @@ -192,6 +192,12 @@ 086A5F442744EE2800FECE02 /* SwiftPamphletAppConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086A5F432744EE2800FECE02 /* SwiftPamphletAppConfig.swift */; }; 086A5F462744EEB900FECE02 /* FundationFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086A5F452744EEB900FECE02 /* FundationFunction.swift */; }; 086A5F522744EF4C00FECE02 /* ViewComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086A5F512744EF4C00FECE02 /* ViewComponent.swift */; }; + 086BEEF82BF629DB00025307 /* AnyLayout(ap).md in Resources */ = {isa = PBXBuildFile; fileRef = 086BEEF72BF629DB00025307 /* AnyLayout(ap).md */; }; + 086BEEFA2BF6300400025307 /* ViewThatFits(ap).md in Resources */ = {isa = PBXBuildFile; fileRef = 086BEEF92BF6300400025307 /* ViewThatFits(ap).md */; }; + 086BEEFC2BF63A0000025307 /* Layout协议(ap).md in Resources */ = {isa = PBXBuildFile; fileRef = 086BEEFB2BF63A0000025307 /* Layout协议(ap).md */; }; + 086BEEFE2BF644D400025307 /* GeometryReader(ap).md in Resources */ = {isa = PBXBuildFile; fileRef = 086BEEFD2BF644D400025307 /* GeometryReader(ap).md */; }; + 086BEF002BF659E500025307 /* alignmentGuide(ap).md in Resources */ = {isa = PBXBuildFile; fileRef = 086BEEFF2BF659E500025307 /* alignmentGuide(ap).md */; }; + 086BEF022BF65DCD00025307 /* 布局进阶-参考资料(ap).md in Resources */ = {isa = PBXBuildFile; fileRef = 086BEF012BF65DCD00025307 /* 布局进阶-参考资料(ap).md */; }; 0871C6192BA040E5000B620D /* InfoRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0871C6182BA040E5000B620D /* InfoRowView.swift */; }; 0871C61B2BA04D23000B620D /* CategoryRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0871C61A2BA04D23000B620D /* CategoryRowView.swift */; }; 0871C61D2BA05F44000B620D /* InfosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0871C61C2BA05F44000B620D /* InfosView.swift */; }; @@ -465,6 +471,12 @@ 086A5F432744EE2800FECE02 /* SwiftPamphletAppConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftPamphletAppConfig.swift; sourceTree = ""; }; 086A5F452744EEB900FECE02 /* FundationFunction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FundationFunction.swift; sourceTree = ""; }; 086A5F512744EF4C00FECE02 /* ViewComponent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewComponent.swift; sourceTree = ""; }; + 086BEEF72BF629DB00025307 /* AnyLayout(ap).md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "AnyLayout(ap).md"; sourceTree = ""; }; + 086BEEF92BF6300400025307 /* ViewThatFits(ap).md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "ViewThatFits(ap).md"; sourceTree = ""; }; + 086BEEFB2BF63A0000025307 /* Layout协议(ap).md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "Layout协议(ap).md"; sourceTree = ""; }; + 086BEEFD2BF644D400025307 /* GeometryReader(ap).md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "GeometryReader(ap).md"; sourceTree = ""; }; + 086BEEFF2BF659E500025307 /* alignmentGuide(ap).md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "alignmentGuide(ap).md"; sourceTree = ""; }; + 086BEF012BF65DCD00025307 /* 布局进阶-参考资料(ap).md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "布局进阶-参考资料(ap).md"; sourceTree = ""; }; 086D48A02BBB820100835544 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 0871C6182BA040E5000B620D /* InfoRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoRowView.swift; sourceTree = ""; }; 0871C61A2BA04D23000B620D /* CategoryRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryRowView.swift; sourceTree = ""; }; @@ -1126,6 +1138,19 @@ path = ViewComponet; sourceTree = ""; }; + 086BEEF62BF629C300025307 /* 布局进阶 */ = { + isa = PBXGroup; + children = ( + 086BEEF72BF629DB00025307 /* AnyLayout(ap).md */, + 086BEEF92BF6300400025307 /* ViewThatFits(ap).md */, + 086BEEFB2BF63A0000025307 /* Layout协议(ap).md */, + 086BEEFD2BF644D400025307 /* GeometryReader(ap).md */, + 086BEEFF2BF659E500025307 /* alignmentGuide(ap).md */, + 086BEF012BF65DCD00025307 /* 布局进阶-参考资料(ap).md */, + ); + path = "布局进阶"; + sourceTree = ""; + }; 0887A5982BA28F3600131359 /* Guide */ = { isa = PBXGroup; children = ( @@ -1167,6 +1192,7 @@ children = ( 0850AC232BF436E8009FDBBF /* Navigation导航 */, 08D4EBE02BF4A7470031EDC5 /* 布局基础 */, + 086BEEF62BF629C300025307 /* 布局进阶 */, 08BE634927C4BDDB002BC6A8 /* Stack(ap).md */, 08BE635B27C65C7C002BC6A8 /* GroupBox(ap).md */, 08C3BB7F27CE4A8500ACF0FE /* TabView(ap).md */, @@ -1496,6 +1522,7 @@ 08659BDF2BEA4D8C009B7C00 /* SwiftData-模型关系(ap).md in Resources */, 08D8EFFC2BEF9EDE00AA0020 /* 小组件-获取位置权限更新内容(ap).md in Resources */, 084E1A6327B517FC0072BBB6 /* Swift各版本演进(ap).md in Resources */, + 086BEEFA2BF6300400025307 /* ViewThatFits(ap).md in Resources */, 08448F6F279EB56400B61353 /* 度量值(ap).md in Resources */, 0844900C279ECBB400B61353 /* Scheduler(ap).md in Resources */, 08448FA3279EBB1B00B61353 /* 数字(ap).md in Resources */, @@ -1525,10 +1552,12 @@ 08026C472869B26900792EF1 /* 调试(ap).md in Resources */, 08448F8C279EB84800B61353 /* 布局动画(ap).md in Resources */, 08448FC1279EC4B500B61353 /* filter(ap).md in Resources */, + 086BEEFC2BF63A0000025307 /* Layout协议(ap).md in Resources */, 08448F9C279EBA8200B61353 /* 闭包(ap).md in Resources */, 0869233C2BF1BF35006779A3 /* ScrollView-参考资料(ap).md in Resources */, 08448FAF279EC31200B61353 /* 不透明类型(ap).md in Resources */, 08449029279ECEB100B61353 /* SwiftUI是什么(ap).md in Resources */, + 086BEEFE2BF644D400025307 /* GeometryReader(ap).md in Resources */, 08448F79279EB68D00B61353 /* 随机(ap).md in Resources */, 08BE632A27BE220D002BC6A8 /* 全屏模式(ap).md in Resources */, 08448F5B279EA84100B61353 /* macOS共享菜单(ap).md in Resources */, @@ -1595,6 +1624,7 @@ 0850AC062BF2E5DA009FDBBF /* List-移动元素(ap).md in Resources */, 08659BCD2BE9A40A009B7C00 /* 创建@Model模型(ap).md in Resources */, 08449000279ECAE100B61353 /* flatMap(ap).md in Resources */, + 086BEF002BF659E500025307 /* alignmentGuide(ap).md in Resources */, 08D8EFFE2BEFA3E300AA0020 /* 支持多个小组件(ap).md in Resources */, 086923382BF19AB7006779A3 /* scrollTargetBehavior分页滚动(ap).md in Resources */, 08448FAB279EC2B400B61353 /* 枚举(ap).md in Resources */, @@ -1618,6 +1648,7 @@ 085BB77627D22FE300E8F69A /* SwiftUI Canvas(ap).md in Resources */, 08BE635827C63F3A002BC6A8 /* ControlGroup(ap).md in Resources */, 08BE637427CCAB52002BC6A8 /* ScrollView(ap).md in Resources */, + 086BEF022BF65DCD00025307 /* 布局进阶-参考资料(ap).md in Resources */, 086923362BF18918006779A3 /* 滚动到特定的位置(ap).md in Resources */, 08448F73279EB5DF00B61353 /* 文件(ap).md in Resources */, 08448FC8279EC54300B61353 /* If(ap).md in Resources */, @@ -1669,6 +1700,7 @@ 08448FE8279EC84B00B61353 /* 恒等(ap).md in Resources */, 08026C4C2869B39F00792EF1 /* Advanced layout control(ap).md in Resources */, 086A5F0E2744E89100FECE02 /* Preview Assets.xcassets in Resources */, + 086BEEF82BF629DB00025307 /* AnyLayout(ap).md in Resources */, 08D4EBD92BF44C170031EDC5 /* NavigationSplitView(ap).md in Resources */, 08448F0F2799328700B61353 /* css_cn.html in Resources */, 086923312BF171A6006779A3 /* ForEach(ap).md in Resources */, @@ -1883,7 +1915,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 11; DEVELOPMENT_ASSET_PATHS = "\"SwiftPamphletApp/Preview Content\""; DEVELOPMENT_TEAM = 962Z8PV35L; ENABLE_HARDENED_RUNTIME = YES; @@ -1897,7 +1929,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 6.4.5; + MARKETING_VERSION = 6.4.6; OTHER_LDFLAGS = ( "-Xlinker", "-interposable", @@ -1926,7 +1958,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 11; DEVELOPMENT_ASSET_PATHS = "\"SwiftPamphletApp/Preview Content\""; DEVELOPMENT_TEAM = 962Z8PV35L; ENABLE_HARDENED_RUNTIME = YES; @@ -1940,7 +1972,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 6.4.5; + MARKETING_VERSION = 6.4.6; PRODUCT_BUNDLE_IDENTIFIER = com.starming.SwiftPamphletAppByMing; PRODUCT_NAME = "戴铭的开发小册子"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/SwiftPamphletApp/Guide/View/GuideListView.swift b/SwiftPamphletApp/Guide/View/GuideListView.swift index 6dfca9a23..84aaa0b45 100644 --- a/SwiftPamphletApp/Guide/View/GuideListView.swift +++ b/SwiftPamphletApp/Guide/View/GuideListView.swift @@ -271,6 +271,14 @@ final class GuideListModel { L(t: "Safe Area"), L(t: "布局原理"), ]), + L(t: "布局进阶", sub: [ + L(t: "AnyLayout"), + L(t: "ViewThatFits"), + L(t: "Layout协议"), + L(t: "GeometryReader"), + L(t: "alignmentGuide"), + L(t: "布局进阶-参考资料"), + ]), L(t: "Stack", icon: "square.3.layers.3d"), L(t: "GroupBox", icon: "shippingbox"), L(t: "TabView"), diff --git a/SwiftPamphletApp/InfoOrganizer/Info/InfoRowView.swift b/SwiftPamphletApp/InfoOrganizer/Info/InfoRowView.swift index b96b59002..73b9e8e1d 100644 --- a/SwiftPamphletApp/InfoOrganizer/Info/InfoRowView.swift +++ b/SwiftPamphletApp/InfoOrganizer/Info/InfoRowView.swift @@ -18,8 +18,9 @@ struct InfoRowView: View { if let coverImg = info.coverImage { ZStack { Rectangle() - .fill(Color.clear) + .fill(Color(light: .white, dark: .black)) .frame(height: 80) + .cornerRadius(5) GeometryReader { geometry in if coverImg.url.isEmpty == false { NukeImage(width: geometry.size.width, height: geometry.size.height, url: coverImg.url, contentModel: .fill) @@ -34,7 +35,7 @@ struct InfoRowView: View { } } } - .shadow(color: Color(.sRGB, red: 0, green: 0, blue: 0, opacity: 0.9), radius: 1, x: 0, y: 0) + .shadow(color: Color(.sRGB, red: 0, green: 0, blue: 0, opacity: 0.3), radius: 1, x: 0, y: 0) } if info.url.isEmpty == false { HStack { diff --git "a/SwiftPamphletApp/Resource/Guide/SwiftUI/\345\270\203\345\261\200\347\273\204\344\273\266/\345\270\203\345\261\200\345\237\272\347\241\200/Safe Area(ap).md" "b/SwiftPamphletApp/Resource/Guide/SwiftUI/\345\270\203\345\261\200\347\273\204\344\273\266/\345\270\203\345\261\200\345\237\272\347\241\200/Safe Area(ap).md" index 6d157cd6f..69f754908 100644 --- "a/SwiftPamphletApp/Resource/Guide/SwiftUI/\345\270\203\345\261\200\347\273\204\344\273\266/\345\270\203\345\261\200\345\237\272\347\241\200/Safe Area(ap).md" +++ "b/SwiftPamphletApp/Resource/Guide/SwiftUI/\345\270\203\345\261\200\347\273\204\344\273\266/\345\270\203\345\261\200\345\237\272\347\241\200/Safe Area(ap).md" @@ -1,4 +1,7 @@ + +## ignoresSafeArea 忽略安全区域 + 使用 `.ignoresSafeArea()` 可以忽略安全区域。默认是所有方向都忽略。 如果只忽略部分方向,可以按照下面方法做: @@ -14,3 +17,89 @@ .ignoresSafeArea(.container, edges: [.leading, .trailing]) ``` +## safeAreaInset + +`safeAreaInset` 是 SwiftUI 中的一个属性,它允许你将视图放置在安全区域内。"安全区域"是指设备屏幕上的一块区域,这块区域不会被系统界面(如状态栏、导航栏、工具栏、Tab栏等)遮挡。 + +例如,你可以使用 `safeAreaInset` 将一个视图放置在屏幕底部的安全区域内,代码如下: + +```swift +VStack { + Text("Hello, World!") +} +.safeAreaInset(edge: .bottom, spacing: 10) { + Button("Press me") { + print("Button pressed") + } +} +``` + +在这个例子中,"Press me" 按钮会被放置在屏幕底部的安全区域内,而且距离底部有 10 个点的间距。 + +下面是更完整点的例子: + +```swift +struct ContentView: View { + @State var tasks: [TaskModel] = (0...10).map { TaskModel(name: "Task \($0)") } + @State var taskName = "" + @State var isFocused: Bool = false + + var body: some View { + NavigationView { + VStack { + List { + ForEach(tasks) { task in + Text(task.name) + } + } + .listStyle(PlainListStyle()) + .safeAreaInset(edge: .bottom) { + HStack { + TextField("Add task", text: $taskName, onCommit: { + addTask() + }) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .padding(.leading, 10) + + Button(action: { + addTask() + }) { + Image(systemName: "plus") + } + .padding(.trailing, 10) + } + .padding(.bottom, isFocused ? 0 : 10) + .background(Color.white) + } + .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { _ in + withAnimation { + isFocused = true + } + } + .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in + withAnimation { + isFocused = false + } + } + } + .navigationBarTitle("Task List Demo") + } + } + + func addTask() { + if !taskName.isEmpty { + withAnimation { + tasks.append(TaskModel(name: taskName)) + } + taskName = "" + } + } +} + +struct TaskModel: Identifiable { + let id = UUID() + let name: String +} +``` + +用户可以在底部的输入框中输入任务名称,然后点击 "+" 按钮将任务添加到任务清单中。添加的任务会显示在屏幕的上方。当键盘出现或消失时,底部的输入框会相应地移动,以确保不会被键盘遮挡。 \ No newline at end of file diff --git "a/SwiftPamphletApp/Resource/Guide/SwiftUI/\345\270\203\345\261\200\347\273\204\344\273\266/\345\270\203\345\261\200\350\277\233\351\230\266/AnyLayout(ap).md" "b/SwiftPamphletApp/Resource/Guide/SwiftUI/\345\270\203\345\261\200\347\273\204\344\273\266/\345\270\203\345\261\200\350\277\233\351\230\266/AnyLayout(ap).md" new file mode 100644 index 000000000..f40a86c7f --- /dev/null +++ "b/SwiftPamphletApp/Resource/Guide/SwiftUI/\345\270\203\345\261\200\347\273\204\344\273\266/\345\270\203\345\261\200\350\277\233\351\230\266/AnyLayout(ap).md" @@ -0,0 +1,42 @@ + +使用 AnyLayout 包装布局组件,可以在布局之间进行切换,同时保持动画效果。 + +```swift +struct WeatherLayout: View { + @State private var changeLayout = false + + var body: some View { + let layout = changeLayout ? AnyLayout(HStackLayout()) : AnyLayout(VStackLayout()) + + layout { + WeatherView(icon: "sun.max.fill", temperature: 25, color: .yellow) + WeatherView(icon: "cloud.rain.fill", temperature: 18, color: .blue) + WeatherView(icon: "snow", temperature: -5, color: .white) + } + .animation(.default, value: changeLayout) + .onTapGesture { + changeLayout.toggle() + } + } +} + +struct WeatherView: View { + let icon: String + let temperature: Int + let color: Color + + var body: some View { + VStack { + Image(systemName: icon) + .font(.system(size: 80)) + .foregroundColor(color) + Text("\(temperature)°") + .font(.system(size: 50)) + .foregroundColor(color) + } + .frame(width: 120, height: 120) + } +} +``` + +代码中,我们创建了一个 WeatherView 视图,它包含一个天气图标和一个温度标签。然后,我们在 WeatherLayout 视图中使用 AnyLayout 来动态改变布局。用户可以通过点击视图来在水平布局和垂直布局之间切换。 diff --git "a/SwiftPamphletApp/Resource/Guide/SwiftUI/\345\270\203\345\261\200\347\273\204\344\273\266/\345\270\203\345\261\200\350\277\233\351\230\266/GeometryReader(ap).md" "b/SwiftPamphletApp/Resource/Guide/SwiftUI/\345\270\203\345\261\200\347\273\204\344\273\266/\345\270\203\345\261\200\350\277\233\351\230\266/GeometryReader(ap).md" new file mode 100644 index 000000000..82356513e --- /dev/null +++ "b/SwiftPamphletApp/Resource/Guide/SwiftUI/\345\270\203\345\261\200\347\273\204\344\273\266/\345\270\203\345\261\200\350\277\233\351\230\266/GeometryReader(ap).md" @@ -0,0 +1,71 @@ + + +在 SwiftUI 中,有多种方法可以获取和控制视图的尺寸: + +- `frame(width:60, height:60)`:这个方法会为子视图提供一个建议的尺寸,这里是 60 x 60。 +- `fixedSize()`:这个方法会为子视图提供一个未指定模式的建议尺寸,这意味着视图会尽可能地大以适应其内容。 +- `frame(minWidth: 120, maxWidth: 360)`:这个方法会将子视图的需求尺寸控制在指定的范围中,这里是宽度在 120 到 360 之间。 +- `frame(idealWidth: 120, idealHeight: 120)`:这个方法会返回一个需求尺寸,如果当前视图收到为未指定模式的建议尺寸,那么它会返回 120 x 120 的尺寸。 +- `GeometryReader`:`GeometryReader` 会将建议尺寸作为需求尺寸直接返回,这意味着它会充满全部可用区域。你可以使用 `GeometryReader` 来获取其内容的尺寸和位置。 + + +`GeometryReader` 可以获取其内容的尺寸和位置。在这个例子中,我们使用 `GeometryReader` 来获取视图的尺寸,然后打印出来。这对于理解 SwiftUI 的布局系统和调试布局问题非常有用。 + +```swift +extension View { + func logSizeInfo(_ label: String = "") -> some View { + background( + GeometryReader { proxy in + Color.clear + .onAppear(perform: { + debugPrint("\(label) Size: \(proxy.size)") + }) + } + ) + } +} + +struct ContentView: View { + var body: some View { + VStack { + Text("大标题") + .font(.largeTitle) + .logSizeInfo("大标题视图") // 打印视图尺寸 + Text("正文") + .logSizeInfo("正文视图") + } + } +} +``` + +这段代码首先定义了一个 `View` 的扩展,添加了一个 `logSizeInfo(_:)` 方法。这个方法接受一个标签字符串作为参数,然后返回一个新的视图。这个新的视图在背景中使用 `GeometryReader` 来获取并打印视图的尺寸。 + +然后,我们创建了一个 `VStack` 视图,其中包含一个 `Text` 视图。我们为 `Text` 视图调用了 `logSizeInfo(_:)` 方法,以打印其尺寸。 + + +如何利用 `GeometryReader` 来绘制一个圆形? + +```swift +struct CircleView: View { + var body: some View { + GeometryReader { proxy in + Path { path in + let radius = min(proxy.size.width, proxy.size.height) / 2 + let center = CGPoint(x: proxy.size.width / 2, y: proxy.size.height / 2) + path.addArc(center: center, radius: radius, startAngle: .zero, endAngle: .init(degrees: 360), clockwise: false) + } + .fill(Color.blue) + } + } +} +``` + +在这个例子中,我们首先获取 `GeometryReader` 的尺寸,然后计算出半径和中心点的位置。然后,我们使用 `Path` 的 `addArc(center:radius:startAngle:endAngle:clockwise:)` 方法来添加一个圆形路径。最后,我们使用 `fill(_:)` 方法来填充路径,颜色为蓝色。 + +关于 GeometryReader 性能问题 + +GeometryReader 是 SwiftUI 中的一个工具,它可以帮助我们获取视图的大小和位置。但是,它在获取这些信息时,需要等待视图被评估、布局和渲染完成。这就好比你在装修房子时,需要等待墙壁砌好、油漆干燥后,才能测量墙壁的尺寸。这个过程可能需要等待一段时间,而且可能需要多次重复,因为每次墙壁的尺寸改变,都需要重新测量。 + +这就是 GeometryReader 可能会影响性能的原因。它需要等待视图完成一轮的评估、布局和渲染,然后才能获取到尺寸数据,然后可能需要根据这些数据重新调整布局,这就需要再次进行评估、布局和渲染。这个过程可能需要重复多次,导致视图被多次重新评估和布局。 + +但是,随着 SwiftUI 的更新,这个问题已经有所改善。现在,我们可以创建自定义的布局容器,这些容器可以在布局阶段就获取到父视图的建议尺寸和所有子视图的需求尺寸,这样就可以避免反复传递尺寸数据,减少了视图的反复更新。 diff --git "a/SwiftPamphletApp/Resource/Guide/SwiftUI/\345\270\203\345\261\200\347\273\204\344\273\266/\345\270\203\345\261\200\350\277\233\351\230\266/Layout\345\215\217\350\256\256(ap).md" "b/SwiftPamphletApp/Resource/Guide/SwiftUI/\345\270\203\345\261\200\347\273\204\344\273\266/\345\270\203\345\261\200\350\277\233\351\230\266/Layout\345\215\217\350\256\256(ap).md" new file mode 100644 index 000000000..2bb00f8a6 --- /dev/null +++ "b/SwiftPamphletApp/Resource/Guide/SwiftUI/\345\270\203\345\261\200\347\273\204\344\273\266/\345\270\203\345\261\200\350\277\233\351\230\266/Layout\345\215\217\350\256\256(ap).md" @@ -0,0 +1,42 @@ + + + +通过实现 Layout 协议,创建一个水平堆栈布局,其中所有子视图的宽度都相等。 + +```swift +struct OptimizedEqualWidthHStack: Layout { + func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) -> CGSize { + if subviews.isEmpty { return .zero } + let maxSubviewSize = calculateMaxSize(subviews: subviews) + let totalSpacing = calculateSpacing(subviews: subviews).reduce(0, +) + return CGSize(width: maxSubviewSize.width * CGFloat(subviews.count) + totalSpacing, height: maxSubviewSize.height) + } + + func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) { + if subviews.isEmpty { return } + let maxSubviewSize = calculateMaxSize(subviews: subviews) + let spacings = calculateSpacing(subviews: subviews) + let placementProposal = ProposedViewSize(width: maxSubviewSize.width, height: maxSubviewSize.height) + var nextX = bounds.minX + maxSubviewSize.width / 2 + for index in subviews.indices { + subviews[index].place(at: CGPoint(x: nextX, y: bounds.midY), anchor: .center, proposal: placementProposal) + nextX += maxSubviewSize.width + spacings[index] + } + } + + private func calculateMaxSize(subviews: Subviews) -> CGSize { + return subviews.map { $0.sizeThatFits(.unspecified) }.reduce(.zero) { CGSize(width: max($0.width, $1.width), height: max($0.height, $1.height)) } + } + + private func calculateSpacing(subviews: Subviews) -> [CGFloat] { + return subviews.indices.map { $0 < subviews.count - 1 ? subviews[$0].spacing.distance(to: subviews[$0 + 1].spacing, along: .horizontal) : 0 } + } +} +``` + +上面这段代码中 sizeThatFits 方法计算并返回布局容器需要的大小,以便排列其子视图。它首先检查子视图数组是否为空,如果为空则返回 .zero。然后,它计算子视图的最大尺寸和总间距,最后返回一个 CGSize 对象,其宽度等于最大子视图宽度乘以子视图数量加上总间距,高度等于最大子视图高度。 + +placeSubviews 方法将子视图放置在布局容器中。它首先检查子视图数组是否为空,如果为空则返回。然后,它计算子视图的最大尺寸和间距,然后遍历子视图数组,将每个子视图放置在布局容器中的适当位置。 + +calculateMaxSize 和 calculateSpacing 是两个私有方法,用于计算子视图的最大尺寸和间距。 + diff --git "a/SwiftPamphletApp/Resource/Guide/SwiftUI/\345\270\203\345\261\200\347\273\204\344\273\266/\345\270\203\345\261\200\350\277\233\351\230\266/ViewThatFits(ap).md" "b/SwiftPamphletApp/Resource/Guide/SwiftUI/\345\270\203\345\261\200\347\273\204\344\273\266/\345\270\203\345\261\200\350\277\233\351\230\266/ViewThatFits(ap).md" new file mode 100644 index 000000000..affe7a827 --- /dev/null +++ "b/SwiftPamphletApp/Resource/Guide/SwiftUI/\345\270\203\345\261\200\347\273\204\344\273\266/\345\270\203\345\261\200\350\277\233\351\230\266/ViewThatFits(ap).md" @@ -0,0 +1,78 @@ + +`ViewThatFits` 是一个自动选择最适合当前屏幕大小的子视图进行显示的视图。它会根据可用空间的大小来决定如何布局和显示子视图。 + +`ViewThatFits` 是一个在 SwiftUI 中用于选择最适合显示的视图的组件。它的工作原理如下: + +- 首先,`ViewThatFits` 会测量在特定轴(水平或垂直)或两个轴(水平和垂直)上的可用空间。这是通过 SwiftUI 的布局系统来完成的,该系统提供了当前视图的大小和位置信息。 + +- 接着,`ViewThatFits` 会测量第一个视图的大小。这是通过调用视图的 `measure(in:)` 方法来完成的,该方法返回一个包含视图理想大小的 `CGSize` 值。 + +- 如果第一个视图的大小适合可用空间,`ViewThatFits` 就会选择并放置这个视图。放置视图是通过调用视图的 `layout(in:)` 方法来完成的,该方法接受一个 `CGRect` 值,该值定义了视图在其父视图中的位置和大小。 + +- 如果第一个视图的大小不适合可用空间,`ViewThatFits` 会继续测量第二个视图的大小。如果第二个视图的大小适合可用空间,`ViewThatFits` 就会选择并放置这个视图。 + +- 如果所有视图的大小都不适合可用空间,`ViewThatFits` 会选择并放置 `ViewBuilder` 闭包中的最后一个视图。`ViewBuilder` 是一个特殊的闭包,它可以根据其内容动态创建视图。 + +```swift +ViewThatFits(in: .horizontal) { + Text("晴天,气温25°") // 宽度在200到300之间 + .font(.title) + .foregroundColor(.yellow) + Text("晴天,25°") // 宽度在150到200之间 + .font(.title) + .foregroundColor(.gray) + Text("晴25") // 宽度在100到150之间 + .font(.title) + .foregroundColor(.white) +} +.border(Color.green) // ViewThatFits所需的大小 +.frame(width:200) +.border(Color.orange) // 父视图提议的大小 +``` + +在不同的宽度下,ViewThatFits 会选择不同的视图进行显示。在上面的示例中,当父视图的宽度在100到150之间时,ViewThatFits 会选择显示 "晴25" 这个视图。 + +通过 ViewThatFits 来确定内容是否可滚动。 + +```swift +struct ContentView: View { + @State var step: CGFloat = 3 + var count: Int { + Int(step) + } + + var body: some View { + VStack(alignment:.leading) { + Text("数量: \(count)") + .font(.title) + .foregroundColor(.blue) + Stepper("数量", value: $step, in: 3...20) + + ViewThatFits { + content + ScrollView(.horizontal,showsIndicators: true) { + content + } + } + } + .padding() + } + + var content: some View { + HStack { + ForEach(0 ..< count, id: \.self) { i in + Rectangle() + .fill(Color.green) + .frame(width: 30, height: 30) + .overlay( + Text("\(i)") + .font(.headline) + .foregroundColor(.white) + ) + } + } + } +} +``` + + diff --git "a/SwiftPamphletApp/Resource/Guide/SwiftUI/\345\270\203\345\261\200\347\273\204\344\273\266/\345\270\203\345\261\200\350\277\233\351\230\266/alignmentGuide(ap).md" "b/SwiftPamphletApp/Resource/Guide/SwiftUI/\345\270\203\345\261\200\347\273\204\344\273\266/\345\270\203\345\261\200\350\277\233\351\230\266/alignmentGuide(ap).md" new file mode 100644 index 000000000..1b8a1ba9b --- /dev/null +++ "b/SwiftPamphletApp/Resource/Guide/SwiftUI/\345\270\203\345\261\200\347\273\204\344\273\266/\345\270\203\345\261\200\350\277\233\351\230\266/alignmentGuide(ap).md" @@ -0,0 +1,34 @@ + + +`alignmentGuide`是SwiftUI中的一个修饰符,它允许你自定义视图的对齐方式。你可以使用它来调整视图在其父视图或同级视图中的位置。 + +当你在一个视图上应用`alignmentGuide`修饰符时,你需要提供一个对齐标识符和一个闭包。对齐标识符定义了你想要调整的对齐方式(例如,`.leading`,`.trailing`,`.center`等)。闭包接收一个参数,这个参数包含了视图的尺寸,你可以使用这个参数来计算对齐指南的偏移量。 + +举个例子: + +```swift +struct ContentView: View { + var body: some View { + HStack(alignment: .top) { + CircleView() + .alignmentGuide(.top) { vd in + vd[.top] + 50 + } + CircleView() + } + .padding() + .border(Color.gray) + } + + struct CircleView: View { + var body: some View { + Circle() + .fill(Color.mint) + .frame(width: 50, height: 50) + } + } +} +``` + +在HStack中,第一个CircleView使用了.alignmentGuide修饰符,这使得它在顶部对齐时向下偏移了50个单位。 + diff --git "a/SwiftPamphletApp/Resource/Guide/SwiftUI/\345\270\203\345\261\200\347\273\204\344\273\266/\345\270\203\345\261\200\350\277\233\351\230\266/\345\270\203\345\261\200\350\277\233\351\230\266-\345\217\202\350\200\203\350\265\204\346\226\231(ap).md" "b/SwiftPamphletApp/Resource/Guide/SwiftUI/\345\270\203\345\261\200\347\273\204\344\273\266/\345\270\203\345\261\200\350\277\233\351\230\266/\345\270\203\345\261\200\350\277\233\351\230\266-\345\217\202\350\200\203\350\265\204\346\226\231(ap).md" new file mode 100644 index 000000000..1db8bd509 --- /dev/null +++ "b/SwiftPamphletApp/Resource/Guide/SwiftUI/\345\270\203\345\261\200\347\273\204\344\273\266/\345\270\203\345\261\200\350\277\233\351\230\266/\345\270\203\345\261\200\350\277\233\351\230\266-\345\217\202\350\200\203\350\265\204\346\226\231(ap).md" @@ -0,0 +1,18 @@ + +## WWDC + +23 +- [Go beyond the window with SwiftUI - WWDC23 - Videos - Apple Developer](https://developer.apple.com/wwdc23/10111) + +22 +- [Bring multiple windows to your SwiftUI app - WWDC22 - Videos - Apple Developer](https://developer.apple.com/wwdc22/10061) 为您的 SwiftUI App 添加多个窗口 + +20 +- [Stacks, Grids, and Outlines in SwiftUI - WWDC20 - Videos - Apple Developer](https://developer.apple.com/wwdc20/10031) SwiftUI 中的叠放、网格和大纲 +- [How to make your app look great on every screen - Discover - Apple Developer](https://developer.apple.com/news/?id=nixcb564) How to make your app look greate on every screen + +## 官方接口文档 +- [Layout fundamentals | 接口](https://developer.apple.com/documentation/swiftui/layout-fundamentals) +- [Layout adjustments | 接口](https://developer.apple.com/documentation/swiftui/layout-adjustments) +- [Custom layout | 接口](https://developer.apple.com/documentation/swiftui/custom-layout) +- [View groupings | 接口](https://developer.apple.com/documentation/swiftui/view-groupings) diff --git "a/SwiftPamphletApp/Resource/Guide/SwiftUI/\346\225\260\346\215\256\351\233\206\345\220\210\347\273\204\344\273\266/Grid(ap).md" "b/SwiftPamphletApp/Resource/Guide/SwiftUI/\346\225\260\346\215\256\351\233\206\345\220\210\347\273\204\344\273\266/Grid(ap).md" index 279ed9e66..f8272f3f4 100644 --- "a/SwiftPamphletApp/Resource/Guide/SwiftUI/\346\225\260\346\215\256\351\233\206\345\220\210\347\273\204\344\273\266/Grid(ap).md" +++ "b/SwiftPamphletApp/Resource/Guide/SwiftUI/\346\225\260\346\215\256\351\233\206\345\220\210\347\273\204\344\273\266/Grid(ap).md" @@ -13,6 +13,7 @@ struct ContentView: View { Text("Tropical") Text("Mango") Text("Pineapple") + .gridCellColumns(2) } GridRow(alignment: .bottom) { Text("Leafy") @@ -25,3 +26,46 @@ struct ContentView: View { } ``` +`gridCellAnchor` 可以让 GridRow 给自己设置对齐方式。 + +`gridCellColumns()` modifier 可以让一个单元格跨多列。 + +GridRow 的间距通过 Grid 的 `horizontalSpacing` 和 `verticalSpacing` 参数来控制。 + +```swift +struct ContentView: View { + let numbers: [[Int]] = [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9] + ] + + var body: some View { + Grid(horizontalSpacing: 0, verticalSpacing: 0) { + ForEach(numbers.indices, id: \.self) { i in + GridRow { + ForEach(numbers[i].indices, id: \.self) { j in + Text("\(numbers[i][j])") + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.gray.opacity(0.2)) + .border(Color.gray, width: 0.5) + } + } + } + } + } +} +``` + +按照以上代码这样写,每个数字 GridRow 之间的间隔就是0了。 + +空白的单元格可以这样写: + +```swift +Color.clear + .gridCellUnsizedAxes([.horizontal, .vertical]) +``` + + + +