普通视图

发现新文章,点击刷新页面。
昨天以前掘金专栏-Swift社区周报

SwiftUI 新容器视图 API 深度解析:轻松构建自定义布局

作者 展菲
2024年9月30日 11:13

前言

自 SwiftUI 的第一个版本发布以来,它就拥有了几种容器视图。最常用的有 HStack、VStack、List 等。今年,Apple 引入了新的 API,使我们能够以全新的方式构建自定义容器视图。本周,我们将学习 SwiftUI 新的分解 API 的优势。

容器视图

容器视图就是一个可以包含其他视图的视图。我们可以使用 @ViewBuilder 闭包轻松定义一个容器视图。以下是一个示例:

struct Card<Content: View>: View {
    @ViewBuilder var content: Content
    
    var body: some View {
        VStack {
            content
        }
        .padding()
        .background(Material.regular, in: .rect(cornerRadius: 8))
        .shadow(radius: 4)
    }
}

如上面的例子所示,我们创建了 Card 视图,它是一个用于容纳任何 SwiftUI 视图的容器视图。它使用 @ViewBuilder 闭包包裹了内容,并添加了一个圆角背景和阴影。

struct ContentView: View {
    var body: some View {
        Card {
            Text("Hello, World!")
            Text("My name is Majid Jabrayilov")
        }
    }
}

这个 Card 类型使用起来非常简单。你只需创建一个 Card,并使用闭包提供内容。通过在 Card 容器视图内嵌入不同的视图,你可以在应用的多个屏幕中复用它。

这是使用容器视图的主要优势之一:你可以通过将共享的功能封装在容器视图中,在应用的不同地方重复使用它们。

想了解更多关于 @ViewBuilder 闭包的内容,可以查看我关于 “SwiftUI 中 @ViewBuilder 的强大功能” 的文章。

使用 ViewBuilder

@ViewBuilder 闭包让我们可以轻松地组合多个视图,并将一个视图嵌入到另一个视图中。但是如何从 @ViewBuilder 闭包中提取子视图呢?SwiftUI 引入了新的 API,允许我们重新组合视图。例如,我们可以从通过 @ViewBuilder 闭包构建的内容视图中提取子视图,并根据需要将它们放置。

struct Carousel<Content: View>: View {
    @ViewBuilder var content: Content
    
    var body: some View {
        ScrollView(.horizontal) {
            LazyHStack {
                ForEach(subviews: content) { subview in
                    subview
                        .containerRelativeFrame(.horizontal)
                }
            }
            .scrollTargetLayout()
        }
        .scrollTargetBehavior(.viewAligned)
        .contentMargins(16)
    }
}

如上面的示例所示,我们使用了带有 subviews 参数的 ForEach 视图,这使我们能够提取内容视图的子视图并对它们进行迭代。

struct ContentView: View {
    var body: some View {
        Carousel {
            Color.yellow
            Color.orange
            Color.red
            Color.blue
            Color.green
        }
    }
}

SwiftUI 使用特定的 Subview 类型来公开提取视图的实例。它符合 View 协议,因此我们仍然可以附加额外的 SwiftUI 视图修饰符。它还为我们提供了 id 属性,这是一个唯一标识符,以及与特定视图关联的容器值。我们将在接下来的文章中更多讨论容器值。

访问子视图

另一种新的 API 允许我们通过索引访问子视图,而不是使用 ForEach 视图进行迭代。

struct Magazine<Content: View>: View {
    @ViewBuilder var content: Content
    
    var body: some View {
        ScrollView {
            Group(subviews: content) { subviews in
                if !subviews.isEmpty {
                    subviews[0]
                        .padding(.horizontal)
                        .containerRelativeFrame(.vertical) { length, _ in
                            return length / 3
                        }
                }
                
                if subviews.count > 1 {
                    ScrollView(.horizontal) {
                        LazyHStack {
                            ForEach(subviews[1...], id: \.id) { subview in
                                subview
                                    .containerRelativeFrame([.horizontal, .vertical])
                            }
                        }
                        .scrollTargetLayout()
                    }
                    .scrollTargetBehavior(.viewAligned)
                    .contentMargins(16)
                }
            }
        }
    }
}

在上面的示例中,我们使用了带有 subviews 参数的 Group 视图,它允许我们将子视图提取到一个名为 SubviewsCollection 的集合类型中。SubviewsCollection 类型符合 RandomAccessCollection 协议,并为我们提供了通过索引访问的功能。

组合子视图

如你所见,我们使用 Group 视图来分解内容视图,然后以另一种方式组合子视图。我们还利用了 id 参数的功能,允许我们使用 ForEach 视图与普通数据一起工作。

struct ContentView: View {
    var body: some View {
        Magazine {
            Color.yellow
            Color.orange
            Color.red
            Color.blue
            Color.green
        }
    }
}

可运行的 Demo

根据文章内容,我将提供一个可以展示如何使用 SwiftUI 新的容器视图 API 构建自定义视图的简单示例,包含 CardCarouselMagazine 容器视图。

import SwiftUI

// 定义 Card 视图,作为一个基本的容器视图
struct Card<Content: View>: View {
    @ViewBuilder var content: Content
    
    var body: some View {
        VStack {
            content
        }
        .padding()
        .background(Material.regular, in: RoundedRectangle(cornerRadius: 8))
        .shadow(radius: 4)
    }
}

// 定义 Carousel 视图,横向滚动的自定义容器视图
struct Carousel<Content: View>: View {
    @ViewBuilder var content: Content
    
    var body: some View {
        ScrollView(.horizontal) {
            LazyHStack {
                ForEach(subviews: content) { subview in
                    subview
                        .containerRelativeFrame(.horizontal)
                }
            }
            .scrollTargetLayout()
        }
        .scrollTargetBehavior(.viewAligned)
        .contentMargins(16)
    }
}

// 定义 Magazine 视图,具有垂直和水平组合布局的自定义容器视图
struct Magazine<Content: View>: View {
    @ViewBuilder var content: Content
    
    var body: some View {
        ScrollView {
            Group(subviews: content) { subviews in
                // 第一个子视图为大图
                if !subviews.isEmpty {
                    subviews[0]
                        .padding(.horizontal)
                        .containerRelativeFrame(.vertical) { length, _ in
                            return length / 3
                        }
                }
                
                // 其余子视图为横向滚动小图
                if subviews.count > 1 {
                    ScrollView(.horizontal) {
                        LazyHStack {
                            ForEach(subviews[1...], id: \.id) { subview in
                                subview
                                    .containerRelativeFrame([.horizontal, .vertical])
                            }
                        }
                        .scrollTargetLayout()
                    }
                    .scrollTargetBehavior(.viewAligned)
                    .contentMargins(16)
                }
            }
        }
    }
}

// 主视图,使用自定义容器视图
struct ContentView: View {
    var body: some View {
        VStack {
            // 使用 Card 视图
            Card {
                Text("SwiftUI 容器视图示例")
                    .font(.headline)
                Text("使用 Card 容器轻松复用视图")
            }
            .padding()
            
            // 使用 Carousel 视图
            Carousel {
                Color.yellow
                Color.orange
                Color.red
                Color.blue
                Color.green
            }
            .frame(height: 100)
            .padding()
            
            // 使用 Magazine 视图
            Magazine {
                Color.pink
                Color.purple
                Color.teal
                Color.mint
            }
            .frame(height: 300)
        }
        .padding()
    }
}

// 主应用入口
@main
struct ContainerViewDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

功能概述

  1. Card:一个简单的容器视图,可以包裹任何内容并添加背景和阴影。你可以在应用中的多个地方使用该容器来保持一致的样式。
  2. Carousel:一个横向滚动的容器视图,可以自动排列并展示内容,适合展示横向滑动的图像或视图。
  3. Magazine:一个自定义的容器视图,允许你将第一个子视图设置为大图,其他子视图横向排列展示。类似于杂志布局。

运行这个Demo

此代码展示了如何在 SwiftUI 中构建自定义的容器视图,灵活地将不同的布局封装在容器中,以便在应用中多次复用这些布局模式。

总结

通过使用 SwiftUI 新引入的 API 以及容器视图,你可以轻松构建具有良好复用性的自定义布局,提升应用的开发效率和代码可维护性。

提升代码调试技巧:从思维到实践

作者 展菲
2024年9月6日 12:14

摘要

调试是软件开发中的关键部分,它不仅帮助开发者找到代码中的错误,还能提高代码质量和开发效率。本文将从调试工具的使用、错误信息的解读、问题定位以及如何培养高效的调试思维等方面,系统地介绍提升调试技巧的方法,并通过实际案例展示调试过程中的思路和步骤。

引言

在开发过程中,每一位程序员都会不可避免地遇到各种各样的错误和问题。对于初学者来说,调试往往会成为令人头疼的难题,而即使是经验丰富的开发者,也经常会陷入复杂的调试过程中。高效的调试技巧能显著提升问题解决的速度,并减少不必要的挫折感。本文旨在分享一些实用的调试技巧和方法,帮助开发者掌握调试过程中的思路和工具。

调试工具的使用

断点调试

断点调试是最基本的调试方式之一,它允许开发者在代码执行过程中暂时中止程序,并逐步检查变量、表达式和函数的状态。在大多数现代 IDE(如VSCode、PyCharm)中,都内置了断点调试功能。

实践示例代码

def add_numbers(a, b):
    return a + b

def main():
    x = 5
    y = 10
    result = add_numbers(x, y)  # 在这里设置断点
    print(f"The result is {result}")

if __name__ == "__main__":
    main()

在上述代码中,开发者可以通过在 result 赋值的地方设置断点,观察函数的输入和输出,判断是否得到了期望的值。

调试器的使用

调试器不仅允许设置断点,还可以逐步执行代码、查看栈帧、检查变量的值。下面以 Python 的 pdb 为例展示如何手动进行调试。

pdb 调试代码

import pdb

def divide_numbers(a, b):
    pdb.set_trace()  # 进入调试模式
    return a / b

print(divide_numbers(10, 0))

当程序执行到 pdb.set_trace() 时,控制台将进入调试模式,可以使用 n 逐行执行,使用 p 查看变量的值。这对于复杂的函数调试非常有用。

阅读和解读错误信息

分析错误栈

当程序抛出异常时,错误栈(stack trace)是开发者最好的朋友。它提供了错误的源头和发生的位置。理解栈信息能够快速帮助开发者找到问题所在。

代码示例

def divide(a, b):
    return a / b

print(divide(10, 0))

执行该代码会抛出 ZeroDivisionError,并显示错误栈。开发者可以通过分析栈信息,迅速定位错误源。

常见的调试技巧

二分法定位问题

对于大型代码项目,如果没有明确的错误提示,逐行排查显然非常耗时。此时,可以使用“二分法”调试:即将代码分成两部分,测试前半部分,若无错,则继续测试后半部分,直到找到问题所在。

日志调试

日志调试是另一种高效的调试方法。通过记录程序运行过程中的重要信息,开发者可以在不依赖IDE的情况下回溯问题发生时的状态。适当的日志级别(如 INFODEBUGERROR)可以帮助开发者了解程序的运行情况。

日志模块代码

import logging

logging.basicConfig(level=logging.DEBUG)

def multiply_numbers(a, b):
    logging.debug(f"multiply_numbers called with a={a}, b={b}")
    return a * b

result = multiply_numbers(5, 10)
logging.info(f"The result is {result}")

日志可以帮助开发者在无法使用断点或调试器的场景下,追踪程序执行流程并发现问题。

调试的思维方式

从错误中学习

调试不仅仅是发现和修复问题的过程,更是开发者提高自己编程能力的机会。每次错误的出现都是对代码逻辑的挑战,因此我们可以从中学习如何避免类似问题。

假设与验证

高效的调试思维是从假设开始的。在调试时,开发者首先应基于代码行为和日志,推测出问题可能的原因,然后通过修改代码或加入日志,验证自己的假设是否正确。这个过程可以帮助迅速缩小问题范围。

避免盲目猜测

调试的过程中,最忌讳的就是盲目猜测而没有系统性的测试。每次调试时,都应基于已有的线索做出推测,再通过验证来逐步排除错误。

QA环节

问:什么情况下使用断点调试,什么情况下使用日志调试?

答:断点调试适用于调试小规模代码或问题容易复现的场景。而日志调试适用于大规模系统或无法实时附加调试器的场景,例如在生产环境下。

问:如何提升调试效率?

答:调试的关键在于培养一种严谨的思维方式。建议在编码过程中加入合理的错误处理和日志,使用二分法快速定位问题区域,同时逐步培养对错误栈信息的敏感度。

总结

提升调试技巧不仅仅是掌握工具的使用,还需要培养一种严谨的思维方式。通过合理使用断点、日志和调试器,结合假设与验证的思路,开发者可以大幅提升调试效率,快速解决问题。

未来的调试工具将更加智能化,可能引入 AI 技术帮助开发者自动定位问题和推荐解决方案。此外,分布式系统和微服务架构的复杂性日益增加,如何对复杂环境下的错误进行调试将成为新的挑战。掌握调试的基本技巧和思维,依然是面对未来开发挑战的基础能力。

参考资料

使用 Swift 6 语言模式构建 Swift 包

作者 展菲
2024年6月4日 20:21

前言

我最近了解到,Swift 6 的一些重大变更(如完整的数据隔离和数据竞争安全检查)将成为 Swift 6 语言模式的一部分,该模式将在 Swift 6 编译器中作为可选功能启用。

这意味着,当你更新 Xcode 版本或使用 Swift 6 编译器的 Swift 工具链时,除非你明确启用 Swift 6 语言模式,否则你的代码将使用 Swift 5 语言模式进行编译。

在本文中,我将向你展示如何下载和安装 Swift 6 工具链的开发快照,并在构建 Swift 包时启用 Swift 6 语言模式。

下载 Swift 6 工具链

使用 Swift 6 编译器和语言模式构建代码的第一步是下载 Swift 6 开发工具链。

Apple 在 swift.org 网站上提供了从 release/6.0 分支构建的 Swift 编译器版本,适用于多个平台,你可以下载并安装到系统中。

你可以手动执行此操作,但我建议使用像 Swiftenv(用于 macOS)或 Swiftly(用于 Linux)这样的工具来管理你的 Swift 工具链,就像本文中所示的那样。

Swiftenv - macOS

Swiftenv 是一个受 pyenv 启发的 Swift 版本管理器,它允许你轻松安装和管理多个版本的 Swift。

使用 Swiftenv,安装最新的 Swift 6 开发快照只需运行以下命令:

# 安装最新的 Swift 6 开发工具链
swiftenv install 6.0-DEVELOPMENT-SNAPSHOT-2024-04-30-a

# 进入你的 Swift 包目录
cd your-swift-package

# 将 Swift 6 工具链设置为此目录的默认工具链
swiftenv local 6.0-DEVELOPMENT-SNAPSHOT-2024-04-30-a

Swiftly - Linux

如果你在 Linux 机器上构建代码,可以使用 Swift Server Workgroup 的 Swiftly 命令行工具来安装和管理 Swift 工具链,运行以下命令:

# 安装最新的 Swift 6 开发工具链
swiftly install 6.0-DEVELOPMENT-SNAPSHOT-2024-04-30-a

# 将 Swift 6 工具链设置为活动工具链
swiftly use 6.0-DEVELOPMENT-SNAPSHOT-2024-04-30-a

在 SPM 中启用语言模式

让我们考虑一个 Swift 包目标,其代码在使用 Swift 6 编译器和 Swift 6 语言模式编译时会产生错误:

class NonIsolated {
    func callee() async {}
}

actor Isolated {
    let isolated = NonIsolated()
    
    func callee() async {
        await isolated.callee()
    }
}

让我们使用我们之前下载的 Swift 6 工具链并启用 StrictConcurrency 实验功能进行构建:

如你所见,构建结果是警告而不是错误。这是因为默认情况下,Swift 6 编译器使用的是 Swift 5 语言模式,而 Swift 6 语言模式是可选的。

有两种方法可以启用 Swift 6 语言模式:直接从命令行通过将 -swift-version 标志传递给 swift 编译器,或者在包清单文件中指定它。

命令行

要启用 Swift 6 语言模式编译代码,可以使用以下命令:

swift build -Xswiftc -swift-version -Xswiftc 6

包清单文件

你可以通过更新 tools-version 到 6.0 并在包清单文件中添加 swiftLanguageVersions 键来为你的 Swift 包启用 Swift 6 语言模式:

// swift-tools-version: 6.0
import PackageDescription

let package = Package(
    name: "Swift6Examples",
    platforms: [.macOS(.v10_15), .iOS(.v13)],
    products: [
        .library(
            name: "Swift6Examples",
            targets: ["Swift6Examples"]
        )
    ],
    targets: [
        .target(name: "Swift6Examples")
    ],
    swiftLanguageVersions: [.version("6")]
)

输出

正如你所见,当启用了 Swift 6 语言模式后,编译器报告了与数据隔离相关的错误。这些错误表明我们在代码中存在需要修复的并发问题。

结论

Swift 6 带来了许多重要的新特性,如数据隔离和数据竞争安全检查,这些特性有助于编写更安全、更高效的代码。然而,这些新特性并不会自动启用,需要通过 Swift 6 语言模式显式开启。通过下载和安装 Swift 6 工具链,并在命令行或包清单文件中启用 Swift 6 语言模式,我们可以提前体验和适应这些变化。尽管新特性带来了一些学习和调整成本,但它们最终会使我们的代码更加健壮。

❌
❌