【问题标题】:Custom Segmented Controller SwiftUI Frame Issue自定义分段控制器 SwiftUI 框架问题
【发布时间】:2021-03-07 02:48:08
【问题描述】:

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

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

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

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

【问题讨论】:

    标签: swift swiftui


    【解决方案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 -- 查找 //&lt;-- Here 注释的位置。

    除了 PreferenceKey 和 View 扩展之外,没有其他任何改变。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-06-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-01-22
      • 2020-07-03
      • 2015-11-01
      相关资源
      最近更新 更多