SwiftUI 实战一、布局总结

SwiftUI 实战二、List 的运用&交互

背景

咱们在学习一个东西时,总是很浅的入门。尤其 SwiftUI 十分好上手,它的 List 控件运用时看起来很简单。可是在实际运用场景中,则会面临各种莫名的状况。本篇文章产生于在项目中一个常见又复杂的业务场景下,深度运用了 SwiftUI 的 List 控件,以及一些交互场景,遇到了一些问题也都逐个处理。跟着以下例子来看下吧,希望能给你供给一些帮助 ~

SwiftUI 实战二、List 的运用&交互
SwiftUI 实战二、List 的运用&交互

如图所示:是一个常见的搜索功用页面(例如微信的联系人列表),咱们的重点会放在列表的展现和交互。其包含以下常见功用:

  1. 列表的悬停作用;
  2. 列表的展现:section 距离问题、分隔线、翻滚条
  3. 右侧首字母:接触时展现气泡、上下移动手势气泡切换、手势中止气泡消失;
  4. section 联动
    1. 右侧首字母点击时,翻滚到对应 section 方位;
    2. 列表翻滚时,右侧首字母对应选中作用。
  5. cell 侧滑功用。

以上功用完结代码对比:

计划 代码
UIKit 760 行 + 侧滑三方库 22 个类
SwiftUI 335 行 + 侧滑自己完结 280 行

下面逐个来看以上遇到的问题,和对应处理计划。

列表的悬停作用

List {}
.listStyle(.plain)

List 的展现款式同 UITableView.Style 相同,需求悬停的作用咱们运用 .plain。

列表的展现:section 距离问题、分隔线、翻滚条

section 距离

SwiftUI 实战二、List 的运用&交互

如图所示,黄色部分是我的自定义 SectionView,红色线框范围是上个 section 最后一个 cell 和 section B 之间剩余的距离。

原因是同 UITableView,plain 款式下,iOS 15 体系下,section 添加默许高度 sectionHeaderTopPadding。 ① 这儿咱们可以在视图 init 或 onAppear 给 UITableView 全局设置为 0 来处理(为了防止影响到其他地方,可以在 onDisappear 时重置回去):

// UITableView
if #available(iOS 15.0, *) {
    UITableView.appearance().sectionHeaderTopPadding = 0
}

可是,此办法只能处理 iOS 15,iOS 16-17 并未处理。其原因是:

SwiftUI 由于开展不久,所以它的一些控件底层完结是基于 UIKit/AppKit 完结的。 关于 List,在 iOS 16 体系以下的底层完结是 UITableView,之后的体系底层完结则是 UICollectionView(这儿可以通过 Xcode 或 Lookin 查看视图图层来证明)。

② 一些文章供给 iOS 16-17 可以通过设置 UICollectionLayoutListConfiguration 来处理 section 距离问题,如下:

// UICollectionView
if #available(iOS 16.0, *) {
    var layoutConfig = UICollectionLayoutListConfiguration(appearance: .plain)
    layoutConfig.headerMode = .supplementary
    layoutConfig.headerTopPadding = 0 // sectionTopPadding设置为0
    let listLayout = UICollectionViewCompositionalLayout.list(using: layoutConfig)
    UICollectionView.appearance().collectionViewLayout = listLayout
}

可是这儿有个留意点:同 UITableView.appearance() 仅仅改动了很简单的特点不同,UICollectionView 这儿改动的是 collectionViewLayout 布局装备。它会改动全局所有已生成的 collectionView.collectionViewLayout 为该 layout,然后导致其他已有的 UICollectionView 页面布局错乱。

一些文章供给了 UICollectionView.appearance().collectionViewLayout = UICollectionViewFlowLayout() 为默许值的方法,但其原理同上,会改动所有已生成的 collectionView.collectionViewLayout 为该默许值,仍是不对的。处理方法是回到本来页面时,重新设置本来的 collectionView.collectionViewLayout 为本来的 layout。 可是这样很麻烦,其影响很大,简单忽略导致出错(假如你的场景比较简单可以运用这种方法)。

③ 为了防止以上设置全局的 appearance() 影响,可以通过获取当时 List 底层完结的具体 UITableView/UICollectionView 来设置。运用 swiftui-introspect 可以帮助咱们获取到底层完结。

.introspectTableView { tableView in
    // code below
}
.introspectCollectionView { collectionView in
    // code below
}

④ 其实 iOS 17 给咱们供给了一个 API:

List {...}
.listSectionSpacing(.compact)

通过测验正式版 iOS 17.1.1,在 plain 款式下不起作用,在 grouped 款式下有用(便是这么不给力)。

⑤ 以上看下来 iOS 15 可以处理,iOS 16-17 没有得到很方便的处理方法(究竟引进 swiftui-introspect 库的成本较大)。假如的 SectionHeader 很高的话可以在布局时把这个距离算进去(运用 padding(.top, XX))。假如不能,运用“ ScrollView + Lazy”吧(替换十分简单)。

ScrollView(showsIndicators: false) {
    LazyVStack(alignment: .leading, spacing: 0, pinnedViews: [.sectionHeaders], content: {
        // 里面和List写法共同
        ForEach(0..<10) { section in
            Section {
                ForEach(0..<10) { ... }
            } header: {
                ...
            }
        }
    })
}

以上是对 List section 距离的剖析,和一些文章处理计划的实施和问题评论。测验下来是有些折腾,初学不免的,总归对 List 有了更深的认识了。接下来的问题和处理就很明晰啦~

分隔线

开篇图上列表的分隔线是我自己画,体系的分隔线一般和整体 UI 都不搭,所以这儿咱们先把体系的分隔线去掉。 iOS 16 以下体系运用:

// UITableView
UITableView.appearance().separatorStyle = .none
UITableView.appearance().separatorColor = .clear

iOS 16 以上体系可以运用以下修饰符:

List {
    ForEach(store.showList, id: .self) { row in
        searchCell(model: row)
            .listRowSeparator(.hidden) // 躲藏分隔线
    }
}

翻滚条

一般来说,为了漂亮,列表的翻滚指示器也需求去除。和以上相似的: iOS 16 以下体系需求运用:

UITableView.appearance().showsVerticalScrollIndicator = false

iOS 16 以上体系可以运用以下修饰符:

List {}
.scrollIndicators(.hidden)

cell 边距

附上一个小知识点,List 的 cell 默许有边距。操控方法为(iOS 13 开始支持):

List {
    ForEach(store.showList, id: .self) { row in
        searchCell(model: row)
            .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) // 可自在设置 cell 边距
    }
    Spacer().frame(height: 100)
}

以上 List 布局上的问题评论完了,接下来咱们看看交互吧:

右侧首字母:接触时展现气泡、上下移动手势气泡切换、手势中止气泡消失

这儿的完结方法很明朗,其实便是需求** touch began、move、end 监听, 可是 SwiftUI 没有相关 API。尽管 SwiftUI 有 DragGesture 可以监听 onChanged、onEnded,可是获取不到 touch began 相似作用,所以这儿需求凭借 UIKit 的封装**了。

封装代码如下:

import SwiftUI
import UIKit
public struct JKTouchGestureView: UIViewRepresentable {
    var tappedCallback: ((UIGestureRecognizer.State, CGPoint) -> Void)
    public init(tappedCallback: @escaping (UIGestureRecognizer.State, CGPoint) -> Void) {
        self.tappedCallback = tappedCallback
    }
    public func makeUIView(context: UIViewRepresentableContext<JKTouchGestureView>) -> UIView {
        let view = UIView(frame: .zero)
        let gesture = SingleTouchDownGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.tapped(_:)))
        view.addGestureRecognizer(gesture)
        return view
    }
    public class Coordinator: NSObject {
        var tappedCallback: ((UIGestureRecognizer.State, CGPoint) -> Void)
        init(tappedCallback: @escaping ((UIGestureRecognizer.State, CGPoint) -> Void)) {
            self.tappedCallback = tappedCallback
        }
        @objc func tapped(_ gesture: UITapGestureRecognizer) {
            if let view = gesture.view {
                let location = gesture.location(in: view)
                self.tappedCallback(gesture.state, location)
            }
        }
    }
    public func makeCoordinator() -> Coordinator {
        Coordinator(tappedCallback: tappedCallback)
    }
    public func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<JKTouchGestureView>) { }
}
private class SingleTouchDownGestureRecognizer: UIGestureRecognizer {
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
        self.state = .began
    }
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
        self.state = .changed
    }
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
        self.state = .ended
    }
}

运用时:

...
.overlay( // 监听手势
    JKTouchGestureView { state, point in
        ...
    }
)

然后在回调中对 state 不同状况操控:气泡的展现/消失、气泡的方位(气泡箭头对齐字母,这儿对齐完结运用 .alignmentGuide,参阅之前的文章 SwiftUI 初次实战—布局总结)。

section 联动

假如在 section 距离 中挑选运用 ScrollView + Lazy 计划,下面的完结对它相同有用,同 List 或 ScrollView 完结的挑选没有关系。

右侧首字母点击时,翻滚到对应 section 方位

这个很好完结,通过 ScrollViewReader 就可以操控 List 翻滚到指定方位。

// 1. 点击首字母时,改动当时首字母挑选 model
store.selectedSection = new
// 2. 监听当时挑选的 section,ScrollViewReader 翻滚到对应为止
ScrollViewReader { scrollViewProxy in
    List {
        ForEach(store.allList, id: .self) { section in
            Section {
                ...
            } header: {
                ...
            }
            .id(section.id) // ① section.id 作为唯一标识,用于 scrollViewProxy 操控翻滚
        }
        ...
        // ② 监听点击时改动的 当时选中的 section 值
        .onChange(of: store.selectedSection) { _ in
            if !bubbleHandling { return } // 大致逻辑:不在气泡中,阐明是在翻滚list,翻滚是会改动selectedSection,防止循环影响
            withAnimation {
                scrollViewProxy.scrollTo(store.selectedSection.id, anchor: .top) // ③ 翻滚到当时选中的 section 方位
            }
        }
    }
}

列表翻滚时,右侧首字母对应选中作用

这个完结比较困难,由于 iOS 17 之前(iOS 17 有 .scrollPosition,有兴趣可以看之前文章 WWDC23 10159 – Beyond scroll views),SwiftUI 并没有给咱们开放监听列表翻滚方位的 API。网上的一些处理计划有:

  1. 获取底层完结,即这儿 List 的底层完结是 UITableView 或 UICollectionView 来操控,上文说过有现成的三方库。可是这样不免未来底层完结可能会再改动,不便保护;
  2. 获取 contentOffset 然后计算翻滚到的方位,这个也没有体系 API,网上的完结方法都有可以自己封装一个。可是假如 List 中的 cell 高度假如是布局上动态改动的,就很难计算精确,也很麻烦。

这儿咱们运用了监听 section 方位的方法来处理:

List {...}
.onAppear {
    self.listMinY = geometry.frame(in: .global).minY
}
private func sectionHeader(section: XXX) -> some View {
    return ZStack {
        GeometryReader { geometry in
            Color.yellow
            // ① 监听 section 的 frame(这儿运用的是 .global,所以下面比较的时分减去整个 list 在 .global 的 minY)
            .onChange(of: geometry.frame(in: .global)) { newFrame in
            	// 大致逻辑:判别没有气泡展现(阐明在翻滚list)、方位在悬停、且不等于当时正在悬停的model
            	if !bubbleHandling, abs(newFrame.minY - self.listMinY) < 10, section != store.selectedSection {
                	store.selectedSection = section // ② 切换section,改动右边index选中的UI(由于4.1已监听改动并翻滚到对应方位)
              	}
        	}
    	}
        ...
    }
}

如此,section 的翻滚和首字母就完结联动功用了。

cell 侧滑功用

List 中,还有个常见的功用,便是 cell 侧滑。iOS 15 以上有可用的体系 API:

public func swipeActions<T>(edge: HorizontalEdge = .trailing, allowsFullSwipe: Bool = true, @ViewBuilder content: () -> T) -> some View where T : View

参阅了几篇文章,完善了一个封装可供参阅。包含:

  1. 自定义侧滑按钮款式
  2. 可挑选侧滑整条删去
  3. 列表中多个 cell 一起只展现一个侧滑作用
  4. 处理一些文章供给的侧滑手势导致整个 List/ScrollView 的笔直翻滚欠好操作问题

其中第 4 点的处理很值得记载一下,关于再次遇到相似问题有个储备:

如上描述,封装侧滑手势后,整体笔直的翻滚不流畅欠好用,参阅的三篇文章的处理都有这个问题。

  • 处理前,要害手势代码如下:
ZStack {}
.simultaneousGesture(DragGesture(minimumDistance: 10, coordinateSpace: .local)

解说下这行代码:

  1. 答应多个手势一起存在(例如 cell 一般也会需求点击手势 .onTapGesture 做一些操作);
  2. 参数 minimumDistance 是指用户触发该手势的最小距离:假如想侧滑好用,那就越小越好。可是越小越简单把笔直手势接纳进来(也便是影响到了整个List的笔直翻滚);越大很明显便是侧滑越难触发。
  • 处理计划:运用肘子大佬的计划:
// 整体
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local) // 不需求答应多个手势一起存在,运用0更好触发
// 真实的cell内容
content
	.highPriorityGesture( // 把该手势优先级置高
	TapGesture(count: 1),
	including: .subviews
	)
	.contentShape(Rectangle())
	.onTapGesture( // 完结该手势
	count: 1,
	perform: {
	}
	)

代码阐明:

  1. 参数 minimumDistance = 0 更简单触发侧滑手势,优化体会;
  2. 去掉了 .simultaneousGesture,运用一般的 .gesture 修饰符;
  3. 运用 .highPriorityGesture 修饰符把 TapGesture(count: 1) 置为高优先级,并完结该手势。

原因剖析:

  • .simultaneousGesture 答应多个手势一起存在,可是会导致 List 的笔直手势 和 cell DragGesture 手势抵触,并有时分优先被 cell DragGesture 接纳,导致 List 的整体笔直翻滚交互不易操作;
  • List 的整体滑动手势操控不了,又没有相似 lowPriorityGesture 修饰符能把 cell DragGesture 的优先级降低;
  • 所以通过运用 .highPriorityGesture,可以把某个手势设置更高的优先级,确保在抵触时它可以被优先响应。尽管它会影响到 content 视图中的手势,但它不会直接影响到 List 的笔直翻滚手势。这样做的结果是,可以在 content 视图中完结自定义的手势逻辑即 cell DragGesture,而不会干扰 List 的默许笔直翻滚行为。

其实这个剖析也不是很透彻,这个处理计划的确很神奇,很难考虑到这个思路,记载下来之后再遇到手势抵触或许可以参阅。

完整代码如下:

import SwiftUI
// MARK: - JKSwipeActionButton
public struct JKSwipeActionButton: Identifiable {
    public let id = UUID()
    let buttonView: () -> AnyView
    let action: () -> Void
    let width: CGFloat
    public init(buttonView: @escaping () -> AnyView,
                action: @escaping () -> Void,
                width: CGFloat = 60) {
        self.buttonView = buttonView
        self.action = action
        self.width = width
    }
}
public extension View {
    func eraseToAnyView() -> AnyView {
      return AnyView(self)
    }
}
// MARK: - JKSwipeActionView
// Adds custom swipe actions to a given view
public struct JKSwipeActionView: ViewModifier {
    var id: String
    // Buttons at the leading (left-hand) side
    let leading: [JKSwipeActionButton]
    // Can you full swipe the leading side
    let allowsFullSwipeLeading: Bool
    // Buttons at the trailing (right-hand) side
    let trailing: [JKSwipeActionButton]
    // Can you full swipe the trailing side
    let allowsFullSwipeTrailing: Bool
    // To list at the same time only one or none in the side slide
    @Binding var currentUserInteractionID: String?
    private let totalLeadingWidth: CGFloat!
    private let totalTrailingWidth: CGFloat!
    // How much does the user have to swipe at least to reveal buttons on either side, init calculated
    private var minSwipeableWidth: CGFloat = 0
    @State private var offset: CGFloat = 0 {
        didSet {
            currentUserInteractionID = offset == 0 ? nil : self.id
        }
    }
    @State private var prevOffset: CGFloat = 0
    // Make sure the content height is truly rendered
    @State private var contentViewHeight: CGFloat = 0
    init(id: String,
         leading: [JKSwipeActionButton] = [],
         allowsFullSwipeLeading: Bool = false,
         trailing: [JKSwipeActionButton] = [],
         allowsFullSwipeTrailing: Bool = false,
         currentUserInteractionID: Binding<String?>) {
        self.id = id
        self.leading = leading
        self.allowsFullSwipeLeading = allowsFullSwipeLeading && !leading.isEmpty
        self.trailing = trailing
        self.allowsFullSwipeTrailing = allowsFullSwipeTrailing && !trailing.isEmpty
        self._currentUserInteractionID = currentUserInteractionID
        totalLeadingWidth = leading.reduce(0, { partialResult, button in
            partialResult + button.width
        })
        totalTrailingWidth = trailing.reduce(0, { partialResult, button in
            partialResult + button.width
        })
        if let last = trailing.last {
            minSwipeableWidth = last.width * 0.8
        } else if let first = leading.first {
            minSwipeableWidth = first.width * 0.8
        }
    }
    // swiftlint:disable function_body_length
    public func body(content: Content) -> some View {
        // Use a GeometryReader to get the size of the view on which we're adding
        // the custom swipe actions.
        GeometryReader { geo in
            // Place leading buttons, the wrapped content and trailing buttons
            // in an HStack with no spacing.
            HStack(spacing: 0) {
                // If any swiping on the left-hand side has occurred, reveal
                // leading buttons. This also resolves button flickering.
                if offset > 0 {
                    // If the user has swiped enough for it to qualify as a full swipe,
                    // render just the first button across the entire swipe length.
                    if fullSwipeEnabled(edge: .leading, width: geo.size.width) {
                        button(for: leading.first)
                            .frame(width: offset, height: geo.size.height)
                    } else {
                        // If we aren't in a full swipe, render all buttons with widths
                        // proportional to the swipe length.
                        ForEach(leading) { actionView in
                            button(for: actionView)
                                .frame(width: individualButtonWidth(edge: .leading),
                                       height: geo.size.height)
                        }
                    }
                }
                // This is the list row itself
                content
                // Add horizontal padding as we removed it to allow the
                // swipe buttons to occupy full row height.
                    .frame(width: geo.size.width, alignment: .leading)
                    .offset(x: (offset > 0) ? 0 : offset)
                    .background(
                        GeometryReader(content: { proxy in
                            Color.clear
                                .preference(key: SizePreferenceKey.self, value: proxy.size)
                        })
                    )
                    .onPreferenceChange(SizePreferenceKey.self, perform: { value in
                        contentViewHeight = value.height
                    })
                    .highPriorityGesture(
                        TapGesture(count: 1),
                        including: .subviews
                    )
                    .contentShape(Rectangle())
                    // fix list scroll unEnabled
                    .onTapGesture(
                        count: 1,
                        perform: {
                        }
                    )
                // If any swiping on the right-hand side has occurred, reveal
                // trailing buttons. This also resolves button flickering.
                if offset < 0 {
                    Group {
                        // If the user has swiped enough for it to qualify as a full swipe,
                        // render just the last button across the entire swipe length.
                        if fullSwipeEnabled(edge: .trailing, width: geo.size.width) {
                            button(for: trailing.last)
                                .frame(width: -offset, height: geo.size.height)
                        } else {
                            // If we aren't in a full swipe, render all buttons with widths
                            // proportional to the swipe length.
                            ForEach(trailing) { actionView in
                                button(for: actionView)
                                    .frame(width: individualButtonWidth(edge: .trailing),
                                           height: geo.size.height)
                            }
                        }
                    }
                    // The leading buttons need to move to the left as the swipe progresses.
                    .offset(x: offset)
                }
            }
            // animate the view as `offset` changes
            .animation(.spring(), value: offset)
            // allows the DragGesture to work even if there are now interactable
            // views in the row
            .contentShape(Rectangle())
            // The DragGesture distates the swipe. The minimumDistance is there to
            // prevent the gesture from interfering with List vertical scrolling.
            .gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local)
                .onChanged { gesture in
                    // Compute the total swipe based on the gesture values.
                    var total = gesture.translation.width + prevOffset
                    if !allowsFullSwipeLeading {
                        total = min(total, totalLeadingWidth)
                    }
                    if !allowsFullSwipeTrailing {
                        total = max(total, -totalTrailingWidth)
                    }
                    offset = total
                }
                .onEnded { _ in
                    // Adjust the offset based on if the user has swiped enough to reveal
                    // all the buttons or not. Also handles full swipe logic.
                    if offset > minSwipeableWidth && !leading.isEmpty {
                        if !checkAndHandleFullSwipe(for: leading, edge: .leading, width: geo.size.width) {
                            offset = totalLeadingWidth
                        }
                    } else if offset < -minSwipeableWidth && !trailing.isEmpty {
                        if !checkAndHandleFullSwipe(for: trailing, edge: .trailing, width: -geo.size.width) {
                            offset = -totalTrailingWidth
                        }
                    } else {
                        offset = 0
                    }
                    prevOffset = offset
                })
        }
        .frame(height: contentViewHeight)
        .onChange(of: currentUserInteractionID) { (_) in
            if (currentUserInteractionID == nil && (offset == totalLeadingWidth || offset == -totalTrailingWidth))
                || (currentUserInteractionID != nil && currentUserInteractionID != self.id && offset != 0) {
                self.offset = 0
                prevOffset = 0
            }
        }
        // Remove internal row padding to allow the buttons to occupy full row height
        .listRowInsets(EdgeInsets())
    }
    // Checks if full swipe is supported and currently active for the given edge.
    // The current threshold is at half of the row width.
    private func fullSwipeEnabled(edge: Edge, width: CGFloat) -> Bool {
        let threshold = abs(width) / 2
        switch edge {
        case .leading:
            return allowsFullSwipeLeading && offset > threshold
        case .trailing:
            return allowsFullSwipeTrailing && -offset > threshold
        }
    }
    // Creates the view for each JKSwipeActionButton. Also assigns it
    // a tap gesture to handle the click and reset the offset.
    private func button(for button: JKSwipeActionButton?) -> some View {
        button?.buttonView()
            .onTapGesture {
                button?.action()
                offset = 0
                prevOffset = 0
            }
    }
    // Calculates width for each button, proportional to the swipe.
    private func individualButtonWidth(edge: Edge) -> CGFloat {
        switch edge {
        case .leading:
            return (offset > 0) ? (offset / CGFloat(leading.count)) : 0
        case .trailing:
            return (offset < 0) ? (abs(offset) / CGFloat(trailing.count)) : 0
        }
    }
    // Checks if the view is in full swipe. If so, trigger the action on the
    // correct button (left- or right-most one), make it full the entire row
    // and schedule everything to be reset after a while.
    private func checkAndHandleFullSwipe(for collection: [JKSwipeActionButton],
                                         edge: Edge,
                                         width: CGFloat) -> Bool {
        if fullSwipeEnabled(edge: edge, width: width) {
            offset = width * CGFloat(collection.count) * 1.2
            ((edge == .leading) ? collection.first : collection.last)?.action()
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                offset = 0
                prevOffset = 0
            }
            return true
        } else {
            return false
        }
    }
    private enum Edge {
        case leading, trailing
    }
}
private struct SizePreferenceKey: PreferenceKey {
    typealias Value = CGSize
    static var defaultValue: CGSize = .zero
    static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
        value = nextValue()
    }
}
// MARK: - Extensiong
extension View {
    public func jk_swipeActions(id: String = UUID().uuidString,
                                leading: [JKSwipeActionButton] = [],
                                allowsFullSwipeLeading: Bool = false,
                                trailing: [JKSwipeActionButton] = [],
                                allowsFullSwipeTrailing: Bool = false,
                                currentUserInteractionID: Binding<String?>) -> some View {
        modifier(JKSwipeActionView(id: id,
                                   leading: leading,
                                   allowsFullSwipeLeading: allowsFullSwipeLeading,
                                   trailing: trailing,
                                   allowsFullSwipeTrailing: allowsFullSwipeTrailing,
                                   currentUserInteractionID: currentUserInteractionID))
    }
}

业务场景运用:

/// 当时交互的cellId
@State var currentUserInteractionID: String?
// cell
PESportSearchCell(model: model) {
    currentUserInteractionID = nil // 比方点击整个cell或其他操作时:撤销当时侧滑状况
}
.jk_swipeActions(id: model.id.uuidString, trailing: [
    JKSwipeActionButton(buttonView: { // 自定义侧滑 UI
        Image("sport_search_delete")
        .padding(.trailing, 20)
        .eraseToAnyView()
    }, action: {
        // ...
    }, width: 44)
], currentUserInteractionID: $currentUserInteractionID)

总结

  1. SwiftUI 代码写法上更简练、更快
  2. SwiftUI 是逐渐开展的,比方它是逐渐供给更好用的体系 API,比方底层完结上会逐渐运用更好的方法完结。因此在不同体系下,遇到问题时多探索清楚,然后处理问题并留意兼容体系版别
  3. 多实践多落地,在实战中总结学习经验和成长。

参阅文章