自定义分段控制器 SwiftUI 框架问题 [英] Custom Segmented Controller SwiftUI Frame Issue

查看:26
本文介绍了自定义分段控制器 SwiftUI 框架问题的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想在 SwiftUI 中创建一个自定义分段控制器,我发现了一个由这个

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

CustomPicker.swift:

struct CustomPicker:查看{@State var selectedIndex = 0var titles = [项目#1"、项目#2"、项目#3"、项目#4"]私有 var 颜色 = [Color.red, Color.green, Color.blue, Color.purple]@State private var frames = Array(重复:.0,计数:4)var主体:一些视图{虚拟堆栈{ZStack {HStack(间距:4){ForEach(self.titles.indices, id: \.self) { index in按钮(动作:{ self.selectedIndex = index }){文本(self.titles[index]).foregroundColor(.black).font(.system(size: 16, weight: .medium, design: .default)).胆大()}.padding(EdgeInsets(top: 16,leading: 16, bottom: 16, trailing: 16)).background(GeometryReader { geo inColor.clear.onAppear { self.setFrame(index: index, frame: geo.frame(in: .global)) }})}}.背景(胶囊().填充(self.colors[self.selectedIndex].opacity(0.4)).frame(width: self.frames[self.selectedIndex].width,高度:self.frames[self.selectedIndex].height,对齐方式:.topLeading).offset(x: self.frames[self.selectedIndex].minX - self.frames[0].minX),对齐方式:.lead)}.animation(.default).background(Capsule().stroke(Color.gray, lineWidth: 3))}}func setFrame(index: Int, frame: CGRect) {self.frames[index] = 框架}}

ContentView.swift:

struct ContentView: 查看 {@State var itemsList = [Item]()功能加载数据(){if let url = Bundle.main.url(forResource: "Data", withExtension: "json") {做 {让数据=尝试数据(contentsOf:url)让解码器 = JSONDecoder()让 jsonData = 尝试decoder.decode(Response.self, from: data)用于 jsonData.content {self.itemsList.append(post)}} 抓住 {打印(错误:\(错误)")}}}var主体:一些视图{导航视图{虚拟堆栈{文本(项目选择器").font(.system(.title)).胆大()自定义选择器()垫片()滚动视图{虚拟堆栈{ForEach(itemsList) { item inItemView(文本:item.text,用户名:item.username).填充(.领先)}}}.frame(高度:UIScreen.screenHeight - 224)}.onAppear(执行:loadData)}}}

这里的项目文件

解决方案

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

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

struct CustomPicker:查看{@State var selectedIndex = 0var titles = [项目#1"、项目#2"、项目#3"、项目#4"]私有 var 颜色 = [Color.red, Color.green, Color.blue, Color.purple]@State private var frames = Array(重复:.0,计数:4)var主体:一些视图{虚拟堆栈{ZStack {HStack(间距:4){ForEach(self.titles.indices, id: \.self) { index in按钮(动作:{ self.selectedIndex = index }){文本(self.titles[index]).foregroundColor(.black).font(.system(size: 16, weight: .medium, design: .default)).胆大()}.padding(EdgeInsets(顶部:16,领先:16,底部:16,尾随:16)).measure()//<-- 这里.onPreferenceChange(FrameKey.self, perform: { value inself.setFrame(index: index, frame: value)//<-- 每次偏好值改变时都会运行,会在框架更新时发生})}}.背景(胶囊().填充(self.colors[self.selectedIndex].opacity(0.4)).frame(width: self.frames[self.selectedIndex].width,高度:self.frames[self.selectedIndex].height,对齐方式:.topLeading).offset(x: self.frames[self.selectedIndex].minX - self.frames[0].minX),对齐方式:.lead)}.animation(.default).background(Capsule().stroke(Color.gray, lineWidth: 3))}}func setFrame(index: Int, frame: CGRect) {打印(设置框架:\(索引):\(框架)")self.frames[index] = 框架}}struct FrameKey : PreferenceKey {静态变量默认值:CGRect = .zerostatic func reduce(value: inout CGRect, nextValue: () -> CGRect) {值 = 下一个值()}}扩展视图{函数测量()->一些视图{self.background(GeometryReader { 几何在颜色清晰.preference(key: FrameKey.self, value: geometry.frame(in: .global))})}}

注意原来的 .background 调用被取出来,替换为 .measure().onPreferenceChange -- 找哪里//<-- 这里 注释是.

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

I would like to create a custom segmented controller in SwiftUI, and I found one made from this post. After slightly altering the code and putting it into my ContentView, the colored capsule would not fit correctly.

Here is an example of my desired result:

This is the result when I use it in 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

解决方案

The problem with the code as-written is that the GeometryReader value is only sent on onAppear. That means that if any of the views around it change and the view is re-rendered (like when the data is loaded), those frames will be out-of-date.

I solved this by using a PreferenceKey instead, which will run on each render:

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))
        })
    }
}

Note that the original .background call was taken out and was replaced with .measure() and .onPreferenceChange -- look for where the //<-- Here note is.

Besides that and the PreferenceKey and View extension, nothing else is changed.

这篇关于自定义分段控制器 SwiftUI 框架问题的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆