{"id":227,"date":"2020-07-26T18:18:19","date_gmt":"2020-07-26T10:18:19","guid":{"rendered":"http:\/\/123.57.164.21\/?p=227"},"modified":"2020-11-05T23:42:19","modified_gmt":"2020-11-05T15:42:19","slug":"swiftui-scrollview%e9%80%9a%e8%bf%87%e4%bb%a3%e7%a0%81%e6%bb%9a%e5%8a%a8","status":"publish","type":"post","link":"https:\/\/92it.top\/?p=227","title":{"rendered":"SwiftUI ScrollView\u901a\u8fc7\u4ee3\u7801\u6eda\u52a8"},"content":{"rendered":"\n<pre class=\"wp-block-preformatted\">iOS13\uff1a\niOS13\u4e2d\uff0c \u60f3\u8ba9SwiftUI\u7684ScrollView\u6eda\u52a8\u8d77\u6765\u6bd4\u8f83\u56f0\u96be\uff0c \u4e0b\u9762\u7684\u65b9\u6cd5\u4e3b\u8981\u662f\u662f\u901a\u8fc7ListScrollingHelper\uff0c \u83b7\u5f97ScrollView\u7684instance\uff0c \u901a\u8fc7\u83b7\u5f97\u7684instance\u6765\u64cd\u4f5cScrollView\u3002<\/pre>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">struct ContentView: View {\n     \/\/ proxy helper\n    @State var scrollingProxy = ListScrollingProxy() \n    \/\/ \u91cd\u8981\uff0c\u901a\u8fc7\u8fd9\u4e2aBool\u7684flag\u6765\u66f4\u65b0ListScrollingHelper\n    @State var activeFlag: Bool = false\n    var body: some View {\n        VStack {\n            HStack {\n                \/\/  \u901a\u8fc7\u6309\u94ae\u6eda\u52a8\u5230\u9876\u90e8\n                Button(action: { self.scrollingProxy.scrollTo(.top) }) { \/\/ &lt; here\n                    Image(systemName: \"arrow.up.to.line\")\n                      .padding(.horizontal)\n                }\n                 \/\/  \u901a\u8fc7\u6309\u94ae\u6eda\u52a8\u5230\u5e95\u90e8\n                Button(action: { self.scrollingProxy.scrollTo(.end) }) { \/\/ &lt;&lt; here\n                    Image(systemName: \"arrow.down.to.line\")\n                      .padding(.horizontal)\n                }\n            }\n            Divider()\n            ScrollView {\n                ForEach(0 ..&lt; 200) { i in\n                    Text(\"Item \\(i)\")\n                        .background(\n                           ListScrollingHelper(activeFlag: self.activeFlag, proxy: self.scrollingProxy))\n                        )\n                }\n            }\n        }\n    }.onAppear() {\n            \/\/ \u91cd\u8981\uff0c\u5ef6\u8fdf0.5\u79d2\u662f\u4e3a\u4e86\u7b49\u5f85view\u6e32\u67d3\u5b8c\u4ee5\u540e\uff0c\u518d\u8c03\u7528ListScrollingHelper\n            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {\n                \/\/ \u901a\u8fc7\u6539\u53d8\u8fd9\u4e2aflag\uff0c\u6fc0\u6d3bListScrollingHelper\u7684 updateUIView \u65b9\u6cd5\u3002\n                self.activeFlag.toggle()\n            }\n            \/\/ view\u521d\u59cb\u5316\u65f6 \u81ea\u5df1\u6eda\u52a8\u5230\u5e95\u90e8\u3002\n            DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {\n                self.scrollingProxy.scrollTo(.end)\n            }\n        }\n}<\/pre>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">struct ListScrollingHelper: UIViewRepresentable {\n    let activeFlag: Bool\n    let proxy: ListScrollingProxy \/\/ reference type\n\n    func makeUIView(context: Context) -> UIView {\n        return UIView() \/\/ managed by SwiftUI, no overloads\n    }\n\n    func updateUIView(_ uiView: UIView, context: Context) {\n        proxy.catchScrollView(for: uiView) \/\/ here UIView is in view hierarchy\n    }\n}<\/pre>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import Foundation\nimport SwiftUI\n\n\/\/ \u53ef\u4ee5\u5b9e\u73b0UIScrollViewDelegate\u534f\u8bae\nclass ListScrollingProxy: NSObject, UIScrollViewDelegate {\n\n    enum Action {\n        case end\n        case top\n        case point(point: Double) \/\/ &lt;&lt; bonus !!\n    }\n\n    \/\/ weak\u91cd\u8981\uff0c\u8981\u4e0d\u4f1a\u4f1a\u548cscrollView\u53d1\u751f\u5f3a\u5f15\u7528 \u9020\u6210\u5185\u5b58\u6cc4\u9732\n    weak private var scrollView: UIScrollView?\n\n    func catchScrollView(for view: UIView) {\n        if nil == scrollView {\n            scrollView = view.enclosingScrollView()\n            \/\/ \u628a\u6355\u83b7\u5230\u7684scrollView\u7684\u4ee3\u7406\u8bbe\u7f6e\u6210\u8be5\u7c7b\u5bf9\u8c61\n            scrollView?.delegate = self\n        }\n    }\n\n    \/\/ \u5f53scrollView\u6eda\u52a8\u65f6\uff0c \u5373\u53ef\u83b7\u5f97\u8be5scrollView\u7684Offset\n    func scrollViewDidScroll(_ scrollView: UIScrollView) {\n        NotificationCenter.default.post(name: .sendScrollViewOffSet, object: (scrollView.contentOffset.x), userInfo: nil)\n    }\n    func scrollTo(_ action: Action) {\n        if let scroller = scrollView {\n            var rect = CGRect(x: 0, y: 0, width: 1, height: 1)\n            switch action {\n            case .end:\n                rect.origin.x = scroller.contentSize.width +\n                    scroller.contentInset.right + scroller.contentInset.left - 1\n            case .point(let point):\n                rect.origin.x = CGFloat(point) >= scroller.contentSize.width ? scroller.contentSize.width - 1: CGFloat(point)\n            default: {\n                \/\/ default goes to top\n                }()\n            }\n            scroller.scrollRectToVisible(rect, animated: true)\n        }\n    }\n}<\/pre>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">extension UIView {\n    func enclosingScrollView() -> UIScrollView? {\n        var next: UIView? = self\n        repeat {\n            next = next?.superview\n            if let scrollview = next as? UIScrollView {\n                return scrollview\n            }\n        } while next != nil\n        return nil\n    }\n}<\/pre>\n\n\n\n<pre class=\"wp-block-preformatted\">iOS14\uff1a\niOS14\u591a\u4e86ScrollViewReader\uff0c \u5b9e\u73b0\u8d77\u6765\u5c31\u5bb9\u6613\u7684\u591a\u4e86\u3002<\/pre>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">class ScrollToModel: ObservableObject {\n    enum Action {\n        case end\n        case top\n    }\n    @Published var direction: Action? = nil\n}\n\nstruct ContentView: View {\n    @StateObject var vm = ScrollToModel()\n\n    let items = (0..&lt;200).map { $0 }\n    var body: some View {\n        VStack {\n            HStack {\n                Button(action: { vm.direction = .top }) { \/\/ &lt; here\n                    Image(systemName: \"arrow.up.to.line\")\n                      .padding(.horizontal)\n                }\n                Button(action: { vm.direction = .end }) { \/\/ &lt;&lt; here\n                    Image(systemName: \"arrow.down.to.line\")\n                      .padding(.horizontal)\n                }\n            }\n            Divider()\n            \n            ScrollView {\n                ScrollViewReader { sp in\n                    LazyVStack {\n                        ForEach(items, id: \\.self) { item in\n                            VStack(alignment: .leading) {\n                                Text(\"Item \\(item)\").id(item)\n                                Divider()\n                            }.frame(maxWidth: .infinity).padding(.horizontal)\n                        }\n                    }.onReceive(vm.$direction) { action in\n                        guard !items.isEmpty else { return }\n                        withAnimation {\n                            switch action {\n                                case .top:\n                                    sp.scrollTo(items.first!, anchor: .top)\n                                case .end:\n                                    sp.scrollTo(items.last!, anchor: .bottom)\n                                default:\n                                    return\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>iOS13\uff1a iOS13\u4e2d\uff0c \u60f3\u8ba9SwiftUI\u7684ScrollView\u6eda\u52a8\u8d77\u6765\u6bd4\u8f83\u56f0\u96be\uff0c \u4e0b\u9762\u7684\u65b9\u6cd5\u4e3b\u8981\u662f\u662f\u901a [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[5],"tags":[],"_links":{"self":[{"href":"https:\/\/92it.top\/index.php?rest_route=\/wp\/v2\/posts\/227"}],"collection":[{"href":"https:\/\/92it.top\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/92it.top\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/92it.top\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/92it.top\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=227"}],"version-history":[{"count":10,"href":"https:\/\/92it.top\/index.php?rest_route=\/wp\/v2\/posts\/227\/revisions"}],"predecessor-version":[{"id":1187,"href":"https:\/\/92it.top\/index.php?rest_route=\/wp\/v2\/posts\/227\/revisions\/1187"}],"wp:attachment":[{"href":"https:\/\/92it.top\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=227"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/92it.top\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=227"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/92it.top\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=227"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}