自定义分段控制器 SwiftUI 框架问题

时间:2021-03-07 02:48:08

标签: swift swiftui

我想在 SwiftUI 中创建一个自定义的分段控制器,我发现了一个由这个 post 制作的。稍微更改代码并将其放入我的 ContentView 后,彩色胶囊无法正确安装。

这是我想要的结果的示例:

enter image description here

这是我在 ContentView 中使用的结果:

enter image description here

CustomPicker.swift:

struct CustomPicker: View {
    @State var selectedIndex = 0
    var titles = ["Item #1", "Item #2", "Item #3", "Item #4"]
    private var colors = [Color.red, Color.green, Color.blue, Color.purple]
    @State private var frames = Array<CGRect>(repeating: .zero, count: 4)
    
    var body: some View {
        VStack {
            ZStack {
                HStack(spacing: 4) {
                    ForEach(self.titles.indices, id: \.self) { index in
                        Button(action: { self.selectedIndex = index }) {
                            Text(self.titles[index])
                                .foregroundColor(.black)
                                .font(.system(size: 16, weight: .medium, design: .default))
                                .bold()
                        }.padding(EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16)).background(
                            GeometryReader { geo in
                                Color.clear.onAppear { self.setFrame(index: index, frame: geo.frame(in: .global)) }
                            }
                        )
                    }
                }
                .background(
                    Capsule().fill(
                        self.colors[self.selectedIndex].opacity(0.4))
                        .frame(width: self.frames[self.selectedIndex].width,
                               height: self.frames[self.selectedIndex].height, alignment: .topLeading)
                        .offset(x: self.frames[self.selectedIndex].minX - self.frames[0].minX)
                    , alignment: .leading
                )
            }
            .animation(.default)
            .background(Capsule().stroke(Color.gray, lineWidth: 3))
        }
    }
    
    func setFrame(index: Int, frame: CGRect) {
        self.frames[index] = frame
    }
}

ContentView.swift:

struct ContentView: View {
    
    @State var itemsList = [Item]()
    
    func loadData() {
        if let url = Bundle.main.url(forResource: "Data", withExtension: "json") {
            do {
                let data = try Data(contentsOf: url)
                let decoder = JSONDecoder()
                let jsonData = try decoder.decode(Response.self, from: data)
                for post in jsonData.content {
                    self.itemsList.append(post)
                }
            } catch {
                print("error:\(error)")
            }
        }
    }
    
    var body: some View {
        NavigationView {
            VStack {
                Text("Item picker")
                    .font(.system(.title))
                    .bold()
                
                CustomPicker()
                
                Spacer()
                
                ScrollView {
                    VStack {
                        ForEach(itemsList) { item in
                            ItemView(text: item.text, username: item.username)
                                .padding(.leading)
                        }
                    }
                }
                .frame(height: UIScreen.screenHeight - 224)
            }
            .onAppear(perform: loadData)
        }
    }
}

Project file here

1 个答案:

答案 0 :(得分:1)

所编写代码的问题在于 GeometryReader 值仅在 onAppear 上发送。这意味着,如果它周围的任何视图发生变化并且视图被重新渲染(例如加载数据时),这些框架将过时。

我通过使用 PreferenceKey 解决了这个问题,它会在每次渲染时运行:

struct CustomPicker: View {
    @State var selectedIndex = 0
    var titles = ["Item #1", "Item #2", "Item #3", "Item #4"]
    private var colors = [Color.red, Color.green, Color.blue, Color.purple]
    @State private var frames = Array<CGRect>(repeating: .zero, count: 4)
    
    var body: some View {
        VStack {
            ZStack {
                HStack(spacing: 4) {
                    ForEach(self.titles.indices, id: \.self) { index in
                        Button(action: { self.selectedIndex = index }) {
                            Text(self.titles[index])
                                .foregroundColor(.black)
                                .font(.system(size: 16, weight: .medium, design: .default))
                                .bold()
                        }
                        .padding(EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16))
                        .measure() // <-- Here
                        .onPreferenceChange(FrameKey.self, perform: { value in
                            self.setFrame(index: index, frame: value) //<-- this will run each time the preference value changes, will will happen any time the frame is updated
                        })
                    }
                }
                .background(
                    Capsule().fill(
                        self.colors[self.selectedIndex].opacity(0.4))
                        .frame(width: self.frames[self.selectedIndex].width,
                               height: self.frames[self.selectedIndex].height, alignment: .topLeading)
                        .offset(x: self.frames[self.selectedIndex].minX - self.frames[0].minX)
                    , alignment: .leading
                )
            }
            .animation(.default)
            .background(Capsule().stroke(Color.gray, lineWidth: 3))
        }
    }
    
    func setFrame(index: Int, frame: CGRect) {
        print("Setting frame: \(index): \(frame)")
        self.frames[index] = frame
    }
}

struct FrameKey : PreferenceKey {
    static var defaultValue: CGRect = .zero
    
    static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
        value = nextValue()
    }
}

extension View {
    func measure() -> some View {
        self.background(GeometryReader { geometry in
            Color.clear
                .preference(key: FrameKey.self, value: geometry.frame(in: .global))
        })
    }
}

请注意,原来的 .background 调用已被取出并替换为 .measure().onPreferenceChange -- 查找 //<-- Here 注释所在的位置。

除此以及 PreferenceKey 和 View 扩展之外,没有其他任何更改。