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

Posted

技术标签:

【中文标题】自定义分段控制器 SwiftUI 框架问题【英文标题】:Custom Segmented Controller SwiftUI Frame Issue 【发布时间】: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

【问题讨论】:

【参考方案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 扩展之外,没有其他任何改变。

【讨论】:

以上是关于自定义分段控制器 SwiftUI 框架问题的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI 分段控件可定制

SwiftUI:使用matchedGeometryEffect控制视图上的zIndex

SwiftUI 接收自定义事件

SwiftUI选取器选择指标不尊重框架宽度

SwiftUI:自定义SearchBar

在 SwiftUI 中更改分段控制器属性