diff --git a/README.md b/README.md index 42f3e0b34..120fd3b29 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Swift开发的手册,是个 macOS 程序,已上线 App Store [点击安装]( - 手册书签收藏 - 资料收集整理 +- 离线保存资料 - 知识点和资料关联 - 手册、WWDC和资料可搜索 - Github 开发者和仓库信息添加管理 diff --git a/SwiftPamphletApp.xcodeproj/project.pbxproj b/SwiftPamphletApp.xcodeproj/project.pbxproj index e5b8d4f25..c2a0f11bb 100644 --- a/SwiftPamphletApp.xcodeproj/project.pbxproj +++ b/SwiftPamphletApp.xcodeproj/project.pbxproj @@ -163,6 +163,12 @@ 08659BDD2BE9E3AA009B7C00 /* SwiftData-调试(ap).md in Resources */ = {isa = PBXBuildFile; fileRef = 08659BDC2BE9E3AA009B7C00 /* SwiftData-调试(ap).md */; }; 08659BDF2BEA4D8C009B7C00 /* SwiftData-模型关系(ap).md in Resources */ = {isa = PBXBuildFile; fileRef = 08659BDE2BEA4D8C009B7C00 /* SwiftData-模型关系(ap).md */; }; 0868D00B2BDD37280023C871 /* SMGitHub in Frameworks */ = {isa = PBXBuildFile; productRef = 0868D00A2BDD37280023C871 /* SMGitHub */; }; + 086923312BF171A6006779A3 /* ForEach(ap).md in Resources */ = {isa = PBXBuildFile; fileRef = 086923302BF171A6006779A3 /* ForEach(ap).md */; }; + 086923342BF178D9006779A3 /* 固定到滚动视图的顶部(ap).md in Resources */ = {isa = PBXBuildFile; fileRef = 086923332BF178D9006779A3 /* 固定到滚动视图的顶部(ap).md */; }; + 086923362BF18918006779A3 /* 滚动到特定的位置(ap).md in Resources */ = {isa = PBXBuildFile; fileRef = 086923352BF18918006779A3 /* 滚动到特定的位置(ap).md */; }; + 086923382BF19AB7006779A3 /* scrollTargetBehavior分页滚动(ap).md in Resources */ = {isa = PBXBuildFile; fileRef = 086923372BF19AB7006779A3 /* scrollTargetBehavior分页滚动(ap).md */; }; + 0869233A2BF1A490006779A3 /* scrollTransition视觉效果(ap).md in Resources */ = {isa = PBXBuildFile; fileRef = 086923392BF1A490006779A3 /* scrollTransition视觉效果(ap).md */; }; + 0869233C2BF1BF35006779A3 /* ScrollView-参考资料(ap).md in Resources */ = {isa = PBXBuildFile; fileRef = 0869233B2BF1BF35006779A3 /* ScrollView-参考资料(ap).md */; }; 086A5F072744E88E00FECE02 /* SwiftPamphletAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086A5F062744E88E00FECE02 /* SwiftPamphletAppApp.swift */; }; 086A5F0B2744E89100FECE02 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 086A5F0A2744E89100FECE02 /* Assets.xcassets */; }; 086A5F0E2744E89100FECE02 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 086A5F0D2744E89100FECE02 /* Preview Assets.xcassets */; }; @@ -400,6 +406,12 @@ 08659BDA2BE9A834009B7C00 /* SwiftData-版本迁移(ap).md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "SwiftData-版本迁移(ap).md"; sourceTree = ""; }; 08659BDC2BE9E3AA009B7C00 /* SwiftData-调试(ap).md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "SwiftData-调试(ap).md"; sourceTree = ""; }; 08659BDE2BEA4D8C009B7C00 /* SwiftData-模型关系(ap).md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "SwiftData-模型关系(ap).md"; sourceTree = ""; }; + 086923302BF171A6006779A3 /* ForEach(ap).md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "ForEach(ap).md"; sourceTree = ""; }; + 086923332BF178D9006779A3 /* 固定到滚动视图的顶部(ap).md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "固定到滚动视图的顶部(ap).md"; sourceTree = ""; }; + 086923352BF18918006779A3 /* 滚动到特定的位置(ap).md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "滚动到特定的位置(ap).md"; sourceTree = ""; }; + 086923372BF19AB7006779A3 /* scrollTargetBehavior分页滚动(ap).md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "scrollTargetBehavior分页滚动(ap).md"; sourceTree = ""; }; + 086923392BF1A490006779A3 /* scrollTransition视觉效果(ap).md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "scrollTransition视觉效果(ap).md"; sourceTree = ""; }; + 0869233B2BF1BF35006779A3 /* ScrollView-参考资料(ap).md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "ScrollView-参考资料(ap).md"; sourceTree = ""; }; 086A5F032744E88E00FECE02 /* 戴铭的开发小册子.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "戴铭的开发小册子.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 086A5F062744E88E00FECE02 /* SwiftPamphletAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftPamphletAppApp.swift; sourceTree = ""; }; 086A5F0A2744E89100FECE02 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -899,6 +911,27 @@ path = SwiftData; sourceTree = ""; }; + 086923322BF17780006779A3 /* Scroll滚动视图 */ = { + isa = PBXGroup; + children = ( + 08BE637327CCAB52002BC6A8 /* ScrollView(ap).md */, + 086923332BF178D9006779A3 /* 固定到滚动视图的顶部(ap).md */, + 086923352BF18918006779A3 /* 滚动到特定的位置(ap).md */, + 086923372BF19AB7006779A3 /* scrollTargetBehavior分页滚动(ap).md */, + 086923392BF1A490006779A3 /* scrollTransition视觉效果(ap).md */, + 0869233B2BF1BF35006779A3 /* ScrollView-参考资料(ap).md */, + ); + path = "Scroll滚动视图"; + sourceTree = ""; + }; + 0869233D2BF1C2CF006779A3 /* List列表 */ = { + isa = PBXGroup; + children = ( + 08BE635327C63828002BC6A8 /* List(ap).md */, + ); + path = "List列表"; + sourceTree = ""; + }; 086A5EFA2744E88E00FECE02 = { isa = PBXGroup; children = ( @@ -1009,8 +1042,9 @@ 08A7FF2E2BEABE0100E12E5A /* 数据集合组件 */ = { isa = PBXGroup; children = ( - 08BE637327CCAB52002BC6A8 /* ScrollView(ap).md */, - 08BE635327C63828002BC6A8 /* List(ap).md */, + 086923302BF171A6006779A3 /* ForEach(ap).md */, + 086923322BF17780006779A3 /* Scroll滚动视图 */, + 0869233D2BF1C2CF006779A3 /* List列表 */, 08BE636327C886D2002BC6A8 /* LazyVStack和LazyHStack(ap).md */, 08BE636F27C8F6A7002BC6A8 /* LazyVGrid和LazyHGrid(ap).md */, 08026C502869B41B00792EF1 /* table(ap).md */, @@ -1361,6 +1395,7 @@ 08448F8C279EB84800B61353 /* 布局动画(ap).md in Resources */, 08448FC1279EC4B500B61353 /* filter(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 */, 08448F79279EB68D00B61353 /* 随机(ap).md in Resources */, @@ -1368,6 +1403,7 @@ 08448F5B279EA84100B61353 /* macOS共享菜单(ap).md in Resources */, 0844900A279ECB8C00B61353 /* combineLatest(ap).md in Resources */, 08026C4D2869B3A600792EF1 /* Transferable(ap).md in Resources */, + 0869233A2BF1A490006779A3 /* scrollTransition视觉效果(ap).md in Resources */, 08659BC72BE8FD84009B7C00 /* SwiftUI数据流(ap).md in Resources */, 08449011279ECC3E00B61353 /* Combine KVO(ap).md in Resources */, 08448FE2279EC7CF00B61353 /* Nil-coalescing(ap).md in Resources */, @@ -1420,6 +1456,7 @@ 08659BCD2BE9A40A009B7C00 /* 创建@Model模型(ap).md in Resources */, 08449000279ECAE100B61353 /* flatMap(ap).md in Resources */, 08D8EFFE2BEFA3E300AA0020 /* 支持多个小组件(ap).md in Resources */, + 086923382BF19AB7006779A3 /* scrollTargetBehavior分页滚动(ap).md in Resources */, 08448FAB279EC2B400B61353 /* 枚举(ap).md in Resources */, 08522BD627CF3218005FF059 /* Picker(ap).md in Resources */, 08659BD92BE9A80E009B7C00 /* SwiftData多线程(ap).md in Resources */, @@ -1439,6 +1476,7 @@ 085BB77627D22FE300E8F69A /* SwiftUI Canvas(ap).md in Resources */, 08BE635827C63F3A002BC6A8 /* ControlGroup(ap).md in Resources */, 08BE637427CCAB52002BC6A8 /* ScrollView(ap).md in Resources */, + 086923362BF18918006779A3 /* 滚动到特定的位置(ap).md in Resources */, 08448F73279EB5DF00B61353 /* 文件(ap).md in Resources */, 08448FC8279EC54300B61353 /* If(ap).md in Resources */, 08448F7E279EB71D00B61353 /* 单例(ap).md in Resources */, @@ -1484,6 +1522,7 @@ 08026C4C2869B39F00792EF1 /* Advanced layout control(ap).md in Resources */, 086A5F0E2744E89100FECE02 /* Preview Assets.xcassets in Resources */, 08448F0F2799328700B61353 /* css_cn.html in Resources */, + 086923312BF171A6006779A3 /* ForEach(ap).md in Resources */, 08448F5A279EA84100B61353 /* 三栏结构(ap).md in Resources */, 08448F9E279EBAA800B61353 /* 函数(ap).md in Resources */, 08448FEC279EC8BE00B61353 /* 注释(ap).md in Resources */, @@ -1496,6 +1535,7 @@ 08D8EFFA2BEF9C9800AA0020 /* 小组件-远程定时获取数据(ap).md in Resources */, 0858C5C72BEBD230004F4C04 /* ContentUnavailableView(ap).md in Resources */, 08448FD5279EC62700B61353 /* Sets(ap).md in Resources */, + 086923342BF178D9006779A3 /* 固定到滚动视图的顶部(ap).md in Resources */, 08448F75279EB62B00B61353 /* Scanner(ap).md in Resources */, 08BE632C27BE3762002BC6A8 /* Link(ap).md in Resources */, 08448FE0279EC7AF00B61353 /* 三元(ap).md in Resources */, @@ -1691,7 +1731,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_ASSET_PATHS = "\"SwiftPamphletApp/Preview Content\""; DEVELOPMENT_TEAM = 962Z8PV35L; ENABLE_HARDENED_RUNTIME = YES; @@ -1705,7 +1745,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 6.4.2; + MARKETING_VERSION = 6.4.3; OTHER_LDFLAGS = ( "-Xlinker", "-interposable", @@ -1734,7 +1774,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_ASSET_PATHS = "\"SwiftPamphletApp/Preview Content\""; DEVELOPMENT_TEAM = 962Z8PV35L; ENABLE_HARDENED_RUNTIME = YES; @@ -1748,7 +1788,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 6.4.2; + MARKETING_VERSION = 6.4.3; 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 87dac175d..fffe0fd79 100644 --- a/SwiftPamphletApp/Guide/View/GuideListView.swift +++ b/SwiftPamphletApp/Guide/View/GuideListView.swift @@ -218,8 +218,18 @@ final class GuideListModel { L(t: "Image"), ]), L(t: "数据集合组件", sub: [ - L(t: "ScrollView"), - L(t: "List"), + L(t: "ForEach"), + L(t: "Scroll视图", icon: "scroll", sub: [ + L(t: "ScrollView"), + L(t: "固定到滚动视图的顶部",icon: "pin.circle"), + L(t: "滚动到特定的位置"), + L(t: "scrollTargetBehavior分页滚动", icon: "book.pages"), + L(t: "scrollTransition视觉效果"), + L(t: "ScrollView-参考资料"), + ]), + L(t: "List列表", icon: "list.bullet.rectangle.portrait.fill", sub: [ + L(t: "List", icon: "list.bullet.rectangle.portrait"), + ]), L(t: "LazyVStack和LazyHStack"), L(t: "LazyVGrid和LazyHGrid"), L(t: "table"), diff --git a/SwiftPamphletApp/Guide/WWDC/WWDCDetailView.swift b/SwiftPamphletApp/Guide/WWDC/WWDCDetailView.swift index ee379d022..78a14972c 100644 --- a/SwiftPamphletApp/Guide/WWDC/WWDCDetailView.swift +++ b/SwiftPamphletApp/Guide/WWDC/WWDCDetailView.swift @@ -49,10 +49,18 @@ struct WWDCDetailView: View { } VStack(alignment:.leading, spacing: 10) { if let vurl = ss.media.videoOriginalUrl { - if ss.year > 2020 { - VideoPlayer(player: AVPlayer(url: URL(string: vurl)!)) - } else { - Link("视频地址:\(vurl)", destination: URL(string: vurl)!) + if let okVurl = URL(string: vurl) { + if ss.year > 2020 { + #if arch(arm64) + VideoPlayer(player: AVPlayer(url: okVurl)) + #elseif arch(x86_64) + Link("视频地址:\(vurl)", destination: okVurl) + #else + Link("视频地址:\(vurl)", destination: okVurl) + #endif + } else { + Link("视频地址:\(vurl)", destination: okVurl) + } } } if let platforms = ss.platforms { diff --git a/SwiftPamphletApp/InfoOrganizer/Info/EditInfoView.swift b/SwiftPamphletApp/InfoOrganizer/Info/EditInfoView.swift index 6e1b5ca3f..c945a27d7 100644 --- a/SwiftPamphletApp/InfoOrganizer/Info/EditInfoView.swift +++ b/SwiftPamphletApp/InfoOrganizer/Info/EditInfoView.swift @@ -282,6 +282,7 @@ struct EditInfoView: View { private func textAndPreviewView() -> some View { TextEditor(text: $info.des).border() .padding(10) + .contentMargins(.all, 30, for: .scrollContent) .tabItem { Label("文本", systemImage: "circle") } .tag(1) // WebUIView(html: wrapperHtmlContent(content: MarkdownParser().html(from: info.des)), baseURLStr: "") @@ -339,6 +340,12 @@ struct EditInfoView: View { .padding(.leading, 1) } .padding(2) + Button(action: { + showSheet = false + }, label: { + Label("关闭", systemImage: "xmark.circle") + }) + .padding(2) } .padding(20) }) diff --git a/SwiftPamphletApp/InfoOrganizer/Info/InfoListView.swift b/SwiftPamphletApp/InfoOrganizer/Info/InfoListView.swift index 5165aceea..46ea8a48b 100644 --- a/SwiftPamphletApp/InfoOrganizer/Info/InfoListView.swift +++ b/SwiftPamphletApp/InfoOrganizer/Info/InfoListView.swift @@ -157,6 +157,12 @@ struct InfoListView: View { .padding(.leading, 1) } .padding(2) + Button(action: { + showSheet = false + }, label: { + Label("关闭", systemImage: "xmark.circle") + }) + .padding(2) } .padding(20) }) 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/ForEach(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/ForEach(ap).md" new file mode 100644 index 000000000..74cad2de7 --- /dev/null +++ "b/SwiftPamphletApp/Resource/Guide/SwiftUI/\346\225\260\346\215\256\351\233\206\345\220\210\347\273\204\344\273\266/ForEach(ap).md" @@ -0,0 +1,136 @@ + +## 使用 + +在 SwiftUI 中,`ForEach` 是一个结构体,它可以创建一组视图,每个视图都有一个与数据集中的元素相对应的唯一标识符。这对于在列表或其他集合视图中显示数据非常有用。 + +以下视图集会用到 ForEach: + +- List +- ScrollView +- LazyVStack / LazyHStack +- Picker +- Grids (LazyVGrid / LazyHGrid) + +例如,如果你有一个 `BookmarkModel` 的数组,并且你想为每个书签创建一个文本视图,你可以这样做: + +```swift +struct ContentView: View { + var bookmarks: [BookmarkModel] + + var body: some View { + List { + ForEach(bookmarks) { bookmark in + Text(bookmark.name) + } + } + } +} +``` + +`ForEach` 遍历 `bookmarks` 数组,并为每个 `BookmarkModel` 对象创建一个 `Text` 视图。`bookmark` 参数是当前遍历的 `BookmarkModel` 对象。 + +`BookmarkModel` 必须遵循 `Identifiable` 协议,这样 SwiftUI 才能知道如何唯一地标识每个视图。在你的代码中,`BookmarkModel` 已经有一个 `id` 属性,所以你只需要让 `BookmarkModel` 遵循 `Identifiable` 协议即可: + +```swift +final class BookmarkModel: Identifiable { + // your code here +} +``` + +## 使用索引范围进行编号 + +你可以使用 `ForEach` 结构体的另一个版本,它接受一个范围作为其数据源。这个范围可以是一个索引范围,这样你就可以为每个项目编号。 + +例如,如果你有一个 `BookmarkModel` 的数组,并且你想为每个书签创建一个文本视图,并在前面添加一个编号,你可以这样做: + +```swift +struct ContentView: View { + var bookmarks: [BookmarkModel] + + var body: some View { + List { + ForEach(bookmarks.indices, id: \.self) { index in + Text("\(index + 1). \(bookmarks[index].name)") + } + } + } +} +``` + +在这个例子中,`ForEach` 遍历 `bookmarks` 数组的索引,并为每个 `BookmarkModel` 对象创建一个 `Text` 视图。`index` 参数是当前遍历的索引。我们使用 `\(index + 1). \(bookmarks[index].name)` 来创建一个带有编号的文本视图。请注意,我们使用 `index + 1` 而不是 `index`,因为数组的索引是从 0 开始的,但我们通常希望编号是从 1 开始的。 + +## 使用 enumerated 编号 + + `enumerated()`  + +以下是一个例子: + +```swift +struct ContentView: View { + var bookmarks: [BookmarkModel] + + var body: some View { + List { + ForEach(Array(bookmarks.enumerated()), id: \.element.id) { index, bookmark in + Text("\(index). \(bookmark.name)") + } + } + } +} +``` + +我们使用 `Array(bookmarks.enumerated())` 来创建一个元组数组,每个元组包含一个索引和一个 `BookmarkModel` 对象。然后,我们使用 `ForEach` 遍历这个元组数组,并为每个元组创建一个 `Text` 视图。`index` 参数是当前遍历的索引,`bookmark` 参数是当前遍历的 `BookmarkModel` 对象。 + +## 使用 zip 编号 + +`zip(_:_:)` 函数可以将两个序列合并为一个元组序列。你可以使用这个函数和 `ForEach` 结构体来为数组中的每个元素添加一个编号。 + +例如,如果你有一个 `BookmarkModel` 的数组,并且你想为每个书签创建一个文本视图,并在前面添加一个编号,你可以这样做: + +```swift +struct ContentView: View { + var bookmarks: [BookmarkModel] + + var body: some View { + List { + ForEach(Array(zip(1..., bookmarks)), id: \.1.id) { index, bookmark in + Text("\(index). \(bookmark.name)") + } + } + } +} +``` + +写出扩展,方便调用 + +```swift +@dynamicMemberLookup +struct Numbered { + var number: Int + var element: Element + + subscript(dynamicMember keyPath: WritableKeyPath) -> T { + get { element[keyPath: keyPath] } + set { element[keyPath: keyPath] = newValue } + } +} + +extension Sequence { + func numbered(startingAt start: Int = 1) -> [Numbered] { + zip(start..., self) + .map { Numbered(number: $0.0, element: $0.1) } + } +} + +extension Numbered: Identifiable where Element: Identifiable { + var id: Element.ID { element.id } +} +``` + +使用: + +```swift +ForEach(bookmark.numbered()) { numberedBookmark in + Text("\(numberedBookmark.number). \(numberedBookmark.name)") +} +``` 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/List(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/List\345\210\227\350\241\250/List(ap).md" similarity index 100% rename from "SwiftPamphletApp/Resource/Guide/SwiftUI/\346\225\260\346\215\256\351\233\206\345\220\210\347\273\204\344\273\266/List(ap).md" rename to "SwiftPamphletApp/Resource/Guide/SwiftUI/\346\225\260\346\215\256\351\233\206\345\220\210\347\273\204\344\273\266/List\345\210\227\350\241\250/List(ap).md" 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/ScrollView(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/Scroll\346\273\232\345\212\250\350\247\206\345\233\276/ScrollView(ap).md" similarity index 75% rename from "SwiftPamphletApp/Resource/Guide/SwiftUI/\346\225\260\346\215\256\351\233\206\345\220\210\347\273\204\344\273\266/ScrollView(ap).md" rename to "SwiftPamphletApp/Resource/Guide/SwiftUI/\346\225\260\346\215\256\351\233\206\345\220\210\347\273\204\344\273\266/Scroll\346\273\232\345\212\250\350\247\206\345\233\276/ScrollView(ap).md" index ccb9e03b1..d23335a0a 100644 --- "a/SwiftPamphletApp/Resource/Guide/SwiftUI/\346\225\260\346\215\256\351\233\206\345\220\210\347\273\204\344\273\266/ScrollView(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/Scroll\346\273\232\345\212\250\350\247\206\345\233\276/ScrollView(ap).md" @@ -96,29 +96,4 @@ private struct OffsetPreferenceKey: PreferenceKey { } ``` -## 滚动到顶部 -在 SwiftUI 中,目前没有直接的方法可以让 List 滚动到顶部。但你可以使用 ScrollViewReader 和 scrollTo 方法来实现滚动到顶部的功能。以下是一个例子: - -```swift -struct ContentView: View { - let items = Array(1...100) // 你的数据 - var body: some View { - ScrollViewReader { scrollView in - VStack { - ScrollView { - ForEach(items, id: \.self) { item in - Text("Item \(item)") - } - .id(items.first) // 给第一个元素设置 id - } - Button("滚动到顶部") { - withAnimation { - scrollView.scrollTo(items.first, anchor: .top) // 滚动到第一个元素 - } - } - } - } - } -} -``` 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/Scroll\346\273\232\345\212\250\350\247\206\345\233\276/ScrollView-\345\217\202\350\200\203\350\265\204\346\226\231(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/Scroll\346\273\232\345\212\250\350\247\206\345\233\276/ScrollView-\345\217\202\350\200\203\350\265\204\346\226\231(ap).md" new file mode 100644 index 000000000..813674916 --- /dev/null +++ "b/SwiftPamphletApp/Resource/Guide/SwiftUI/\346\225\260\346\215\256\351\233\206\345\220\210\347\273\204\344\273\266/Scroll\346\273\232\345\212\250\350\247\206\345\233\276/ScrollView-\345\217\202\350\200\203\350\265\204\346\226\231(ap).md" @@ -0,0 +1,10 @@ + +## 文档 + +- [ScrollView | Apple Developer Documentation](https://developer.apple.com/documentation/swiftui/scrollview) 官方文档 +- [Scroll views | 接口](https://developer.apple.com/documentation/swiftui/scroll-views) 官方接口文档 + +## WWDC + +23 +- [Beyond scroll views - WWDC23 - Videos - Apple Developer](https://developer.apple.com/wwdc23/10159) 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/Scroll\346\273\232\345\212\250\350\247\206\345\233\276/scrollTargetBehavior\345\210\206\351\241\265\346\273\232\345\212\250(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/Scroll\346\273\232\345\212\250\350\247\206\345\233\276/scrollTargetBehavior\345\210\206\351\241\265\346\273\232\345\212\250(ap).md" new file mode 100644 index 000000000..27a3d8440 --- /dev/null +++ "b/SwiftPamphletApp/Resource/Guide/SwiftUI/\346\225\260\346\215\256\351\233\206\345\220\210\347\273\204\344\273\266/Scroll\346\273\232\345\212\250\350\247\206\345\233\276/scrollTargetBehavior\345\210\206\351\241\265\346\273\232\345\212\250(ap).md" @@ -0,0 +1,55 @@ + +## 按可视尺寸分页 + +`.scrollTargetBehavior(.paging)` 可以让 ScrollView 滚动,滚动一页的范围是 ScrollView 的可视尺寸。 +  +```swift +struct ContentView: View { + var body: some View { + ScrollView(.horizontal) { + LazyHStack { + ForEach(0...20, id: \.self) { i in + colorView() + .frame(width: 300, height: 200) + } + } + } + .scrollTargetBehavior(.paging) + } + + @ViewBuilder + func colorView() -> some View { + [Color.red, Color.yellow, Color.blue, Color.mint, Color.indigo, Color.green].randomElement() + } +} +``` + +## 按容器元素对齐分页 + +使用 `.scrollTargetBehavior(.viewAligned)` 配合 scrollTargetLayout。示例代码如下: + +```swift +struct ContentView: View { + var body: some View { + ScrollView(.horizontal) { + LazyHStack { + ForEach(0...20, id: \.self) { i in + colorView() + .frame(width: 300, height: 200) + } + } + .scrollTargetLayout(isEnabled: true) + } + .scrollTargetBehavior(.viewAligned) + } + + @ViewBuilder + func colorView() -> some View { + [Color.red, Color.yellow, Color.blue, Color.mint, Color.indigo, Color.green].randomElement() + } +} +``` + + + + 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/Scroll\346\273\232\345\212\250\350\247\206\345\233\276/scrollTransition\350\247\206\350\247\211\346\225\210\346\236\234(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/Scroll\346\273\232\345\212\250\350\247\206\345\233\276/scrollTransition\350\247\206\350\247\211\346\225\210\346\236\234(ap).md" new file mode 100644 index 000000000..81ab601c0 --- /dev/null +++ "b/SwiftPamphletApp/Resource/Guide/SwiftUI/\346\225\260\346\215\256\351\233\206\345\220\210\347\273\204\344\273\266/Scroll\346\273\232\345\212\250\350\247\206\345\233\276/scrollTransition\350\247\206\350\247\211\346\225\210\346\236\234(ap).md" @@ -0,0 +1,71 @@ + + +iOS 17 新推出 `.scrollTransition`,用于处理滚动时的动画。 + +`.transition` 用于视图插入和移除视图树时的动画。 + +`.scrollTransition` 会和滚动联合起来进行平滑的过渡动画处理。`.scrollTransition` 可以修改很多属性,比如大小,可见性还有旋转等。 + +`.scrollTransition` 可以针对不同阶段进行处理,目前有三个阶段: + +- `topLeading`: 视图进入 ScrollView 可见区域 +- `identity`: 在可见区域中 +- `bottomTrailing`: 视图离开 ScrollView 可见区域 + +```swift +struct ContentView: View { + var body: some View { + ScrollView(.horizontal) { + LazyHStack { + ForEach(0...20, id: \.self) { i in + colorView() + .frame(width: 300, height: 200) + .scrollTransition { content, phase in + content + .scaleEffect(phase.isIdentity ? 1 : 0.4) + } + } + } + } + } + + @ViewBuilder + func colorView() -> some View { + [Color.red, Color.yellow, Color.blue, Color.mint, Color.indigo, Color.green].randomElement() + } +} +``` + +使用阶段的值 + +```swift +.scrollTransition(.animated(.bouncy)) { content, phase in + content + .scaleEffect(phase.isIdentity ? 1 : phase.value) +} +``` + +不同阶段的产生效果设置 + +```swift +.scrollTransition( + topLeading: .animated, + bottomTrailing: .interactive +) { content, phase in + content.rotationEffect(.radians(phase.value)) +} +``` + +`.rotation3DEffect` 也是支持的。 + +```swift +.scrollTransition(.interactive) { content, phase in + content + .rotation3DEffect( + Angle.degrees(phase.isIdentity ? 0: 120), + axis: (x: 0.9, y: 0.0, z: 0.1)) + .offset(x: phase.value * -300) +} +``` + + 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/Scroll\346\273\232\345\212\250\350\247\206\345\233\276/\345\233\272\345\256\232\345\210\260\346\273\232\345\212\250\350\247\206\345\233\276\347\232\204\351\241\266\351\203\250(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/Scroll\346\273\232\345\212\250\350\247\206\345\233\276/\345\233\272\345\256\232\345\210\260\346\273\232\345\212\250\350\247\206\345\233\276\347\232\204\351\241\266\351\203\250(ap).md" new file mode 100644 index 000000000..4e4f31883 --- /dev/null +++ "b/SwiftPamphletApp/Resource/Guide/SwiftUI/\346\225\260\346\215\256\351\233\206\345\220\210\347\273\204\344\273\266/Scroll\346\273\232\345\212\250\350\247\206\345\233\276/\345\233\272\345\256\232\345\210\260\346\273\232\345\212\250\350\247\206\345\233\276\347\232\204\351\241\266\351\203\250(ap).md" @@ -0,0 +1,17 @@ + +LazyVStack 有个参数 pinnedViews 可以用于固定滚动视图的顶部。 + +```swift +ScrollView { + LazyVStack(alignment: .leading, spacing: 10, pinnedViews: .sectionHeaders) { + Section { + ForEach(books) { book in + BookRowView(book: book) + } + } header: { + HeaderView(title: "小说") + } + .... + } +} +``` 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/Scroll\346\273\232\345\212\250\350\247\206\345\233\276/\346\273\232\345\212\250\345\210\260\347\211\271\345\256\232\347\232\204\344\275\215\347\275\256(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/Scroll\346\273\232\345\212\250\350\247\206\345\233\276/\346\273\232\345\212\250\345\210\260\347\211\271\345\256\232\347\232\204\344\275\215\347\275\256(ap).md" new file mode 100644 index 000000000..49783fd36 --- /dev/null +++ "b/SwiftPamphletApp/Resource/Guide/SwiftUI/\346\225\260\346\215\256\351\233\206\345\220\210\347\273\204\344\273\266/Scroll\346\273\232\345\212\250\350\247\206\345\233\276/\346\273\232\345\212\250\345\210\260\347\211\271\345\256\232\347\232\204\344\275\215\347\275\256(ap).md" @@ -0,0 +1,101 @@ + + +## scrollPostion 版本 + +`scrollPositon(id:)` 比 ScrollViewReader 简单,但是只适用于 ScrollView。数据源遵循 Identifiable,不用显式使用 `id` 修饰符 + +```swift +struct ContentView: View { + @State private var id: Int? + + var body: some View { + VStack { + Button("Scroll to Bookmark 3") { + withAnimation { + id = 3 + } + } + Button("Scroll to Bookmark 13") { + withAnimation { + id = 13 + } + } + ScrollView { + ScrollViewReader { scrollView in + LazyVStack { + ForEach(Bookmark.simpleData()) { bookmark in + Text("\(bookmark.index)") + .id(bookmark.index) + } + + } + } + } + .scrollPosition(id: $id) + .scrollTargetLayout() + } + } + + struct Bookmark: Identifiable,Hashable { + let id = UUID() + let index: Int + + static func simpleData() -> [Bookmark] { + var re = [Bookmark]() + for i in 0...100 { + re.append(Bookmark(index: i)) + } + return re + } + } +} +``` + +scrollTargetLayout 可以获得当前滚动位置。锚点不可配,默认是 center。 + + +## ScrollViewReader 版本 + +ScrollViewReader 这个版本可以适用于 List,也可以配置锚点 + +你可以使用 `ScrollViewReader` 和 `scrollTo(_:anchor:)` 方法来滚动到特定的元素。以下是一个例子: + +```swift +struct ContentView: View { + var bookmarks: [Int] = Array(1...100) + @State private var selectedBookmarkId: Int? + + var body: some View { + VStack { + Button("Scroll to Bookmark 3") { + selectedBookmarkId = 3 + } + Button("Scroll to Bookmark 13") { + selectedBookmarkId = 13 + } + ScrollView { + ScrollViewReader { scrollView in + LazyVStack { + ForEach(bookmarks.indices, id: \.self) { index in + Text("\(bookmarks[index])") + .id(index) + } + .onChange(of: selectedBookmarkId) { oldValue, newValue in + if let newValue = newValue { + withAnimation { + scrollView.scrollTo(newValue, anchor: .top) + } + } + } + } + } + } + } + } +} +``` + +在这个例子中,我们首先创建了一个 `Button`,当点击这个按钮时,`selectedBookmarkId` 的值会被设置为 3。然后,我们创建了一个 `ScrollView`,并在 `ScrollView` 中添加了一个 `ScrollViewReader`。我们在 `ScrollViewReader` 中添加了一个 `LazyVStack`,并使用 `ForEach` 遍历 `bookmarks` 数组的索引,为每个索引创建一个 `Text` 视图。我们使用 `id(_:)` 方法为每个 `Text` 视图设置了一个唯一的 ID。 + +我们使用 `onChange(of:perform:)` 方法来监听 `selectedBookmarkId` 的变化。当 `selectedBookmarkId` 的值改变时,我们会调用 `scrollTo(_:anchor:)` 方法来滚动到特定的元素。`anchor: .top` 参数表示我们希望滚动到的元素位于滚动视图的顶部。 +