Last active
December 12, 2022 16:49
-
-
Save YusukeHosonuma/3ad13e270d3cc7b6c1d7709c4ebe1ae8 to your computer and use it in GitHub Desktop.
After: [SwiftUI] タブがタップされた時に上までスクロールする。あるいはそれを共通化する話。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
struct ContentView: View { | |
@State private var selectedTab: Tab = .first | |
var body: some View { | |
TabContainer(selection: $selectedTab) { tabTappedTwices in | |
ForEach(Tab.allCases) { tab in | |
SampleView( | |
title: tab.title, | |
tabTappedTwice: tabTappedTwices[tab]! | |
) | |
.tabItem(tab.tabItem) | |
.tag(tab) | |
} | |
} | |
} | |
} | |
enum Tab: String, Identifiable, CaseIterable { | |
case first | |
case second | |
var id: String { rawValue } | |
var title: String { | |
switch self { | |
case .first: return "First" | |
case .second: return "Second" | |
} | |
} | |
@ViewBuilder | |
func tabItem() -> some View { | |
switch self { | |
case .first: Label(title, systemImage: "1.circle") | |
case .second: Label(title, systemImage: "2.circle") | |
} | |
} | |
} | |
struct SampleView: View { | |
var title: String | |
var tabTappedTwice: Trigger | |
var body: some View { | |
NavigationView { | |
ScrollViewReader { proxy in | |
List(items) { item in | |
Text(item.title) | |
.id(items.first?.id == item.id ? "top" : nil) | |
} | |
.listStyle(.plain) | |
.onTrigger(of: tabTappedTwice) { | |
withAnimation { | |
proxy.scrollTo("top", anchor: .top) | |
} | |
} | |
} | |
.navigationTitle(title) | |
.navigationBarTitleDisplayMode(.inline) | |
} | |
} | |
} | |
struct Item: Identifiable { | |
var id: Int | |
var title: String { "Item \(id)" } | |
init(_ number: Int) { | |
id = number | |
} | |
} | |
// MARK: Sample | |
private let items: [Item] = (1..<100).map(Item.init) | |
// MARK: Component | |
struct TabContainer<SelectionValue: Hashable & CaseIterable, Content: View>: View { | |
@Binding var selection: SelectionValue | |
@ViewBuilder var content: ([SelectionValue: Trigger]) -> Content | |
@State private var scrollToTops: [SelectionValue: Trigger] = .init( | |
uniqueKeysWithValues: SelectionValue.allCases.map { ($0, .init()) } | |
) | |
var body: some View { | |
TabView(selection: $selection.willSet { | |
if selection == $0 { | |
scrollToTops[selection]?.fire() | |
} | |
}) { | |
content(scrollToTops) | |
} | |
} | |
} | |
struct TabList< | |
Data: RandomAccessCollection, | |
Content: View | |
>: View where Data.Element: Identifiable { | |
var items: Data | |
var scrollToTop: Trigger? | |
var content: (Data.Element) -> Content | |
init( | |
_ items: Data, | |
scrollToTop: Trigger?, | |
content: @escaping (Data.Element) -> Content | |
) { | |
self.items = items | |
self.scrollToTop = scrollToTop | |
self.content = content | |
} | |
var body: some View { | |
ScrollViewReader { proxy in | |
List(items) { item in | |
content(item) | |
.id(items.first?.id == item.id ? "top" : nil) | |
} | |
.scrollToTop(on: scrollToTop, proxy: proxy) | |
.listStyle(.plain) | |
} | |
} | |
} | |
// MARK: Util | |
struct Trigger { | |
private(set) var key: Bool = false | |
mutating func fire() { | |
key.toggle() | |
} | |
} | |
extension View { | |
func onTrigger(of trigger: Trigger?, perform: @escaping () -> Void) -> some View { | |
onChange(of: trigger?.key) { _ in | |
perform() | |
} | |
} | |
} | |
// MARK: Extension | |
extension View { | |
func scrollToTop(on trigger: Trigger?, proxy: ScrollViewProxy) -> some View { | |
onTrigger(of: trigger) { | |
withAnimation { | |
proxy.scrollTo("top", anchor: .top) | |
} | |
} | |
} | |
} | |
extension Binding { | |
func willSet(_ handler: @escaping (Value) -> ()) -> Binding<Value> { | |
.init( | |
get: { wrappedValue }, | |
set: { newValue in | |
handler(newValue) | |
wrappedValue = newValue | |
} | |
) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment