美文网首页Hacking with iOS: SwiftUI Edition
Hacking with iOS: SwiftUI Editio

Hacking with iOS: SwiftUI Editio

作者: 韦弦Zhy | 来源:发表于2020-11-05 22:50 被阅读0次

    \color{red}{\Large \mathbf{Hacking \quad with \quad iOS: SwiftUI \quad Edition}}

    {\Large \mathbf{Flashzilla}}

    给滑动的视图着色

    用户可以向左或向右滑动我们的卡片,以将其标记为正确猜对与否,但这两个方向之间没有视觉上的区别。从 探探 等约会应用中借用控件,我们将向右滑动(他们正确猜出了答案),向左滑动(猜错误)。

    我们将通过两种方式解决此问题:对于具有默认设置的手机,我们将在褪色之前使卡片变成绿色或红色,但是如果用户启用了无色区分设置(辅助功能内),我们将卡片保留为白色和白色。而是在我们的背景上显示一些额外的UI。

    让我们从卡片本身的开始。现在,我们的卡片视图就是在这种背景下创建的:

    RoundedRectangle(cornerRadius: 25, style: .continuous)
        .fill(Color.white)
        .shadow(radius: 10)
    

    我们将用一些更高级的代码替换它:我们将为它提供一个具有相同圆角矩形的背景,除了绿色或红色(取决于手势移动)外,然后使上方的白色填充在拖动运动时变大淡出。

    首先是背景。将其直接添加到shadow()修饰符之前:

    .background(
        RoundedRectangle(cornerRadius: 25, style: .continuous)
            .fill(offset.width > 0 ? Color.green : Color.red)
    )
    

    至于白色填充的不透明度,这将与我们之前添加的opacity()修饰符相似,除了我们将使用1减去手势宽度的1/50而不是2减去手势宽度。这会产生一个非常不错的效果:我们之前使用了2减,因为这意味着该卡在褪色之前必须至少移动50个点,但是对于卡填充,我们将使用1减,以便它开始立即变为彩色。

    以此替换现有的fill()修饰符:

    .fill(
        Color.white
            .opacity(1 - Double(abs(offset.width / 50)))
    )
    

    如果现在运行该应用程序,您会看到卡片从白色融合为红色或绿色,然后逐渐消失。太棒了!

    但是,尽管我们的代码很好,但对于有红/绿盲的人来说,效果并不好——他们会看到卡的亮度发生变化,但不清楚是哪面。

    为了解决这个问题,我们将添加一个环境属性以跟踪是否为此目的使用颜色,然后在该属性为 true 时禁用红色/绿色效果。

    首先,在现有属性之前,将这个新属性添加到CardView中:

    @Environment(\.accessibilityDifferentiateWithoutColor) var differentiateWithoutColor
    

    现在,我们可以将其用于RoundedRectangle的填充和背景,以确保我们平滑地淡化白色。两者都必须使用,这很重要,因为随着卡片淡出,背景色将开始从填充中扩散。

    因此,用以下代码替换当前的RoundedRectangle代码:

    RoundedRectangle(cornerRadius: 25, style: .continuous)
        .fill(
            differentiateWithoutColor
                ? Color.white
                : Color.white
                    .opacity(1 - Double(abs(offset.width / 50)))
    
        )
        .background(
            differentiateWithoutColor
                ? nil
                : RoundedRectangle(cornerRadius: 25, style: .continuous)
                    .fill(offset.width > 0 ? Color.green : Color.red)
        )
        .shadow(radius: 10)
    

    因此,在默认配置下,我们的卡片会逐渐变为绿色或红色,但是在启用“无色差异”后,将不会使用。取而代之的是,我们需要在ContentView中提供一些额外的UI,以明确哪一方是正确的,哪一方是错误的。

    之前,我们在ContentView中制作了一个非常特殊的堆栈结构:我们有一个ZStack,然后是VStack,然后是另一个ZStack。第一个ZStack是最外层的ZStack,它使我们的背景和卡片叠层重叠,并且我们还将在该叠层中放置一些按钮,以便用户可以看到哪一侧是“好”的。

    首先,将此属性添加到ContentView

    @Environment(\.accessibilityDifferentiateWithoutColor) var differentiateWithoutColor
    

    现在,在VStack之后直接添加以下新视图:

    if differentiateWithoutColor {
        VStack {
            Spacer()
    
            HStack {
                Image(systemName: "xmark.circle")
                    .padding()
                    .background(Color.black.opacity(0.7))
                    .clipShape(Circle())
                Spacer()
                Image(systemName: "checkmark.circle")
                    .padding()
                    .background(Color.black.opacity(0.7))
                    .clipShape(Circle())
            }
            .foregroundColor(.white)
            .font(.largeTitle)
            .padding()
        }
    }
    

    这将创建另一个VStack,这次是从一个间隔符开始的,以便将堆栈中的图像推到屏幕底部。围绕着这些条件,它们只有在启用“无色差异”时才会显示,因此大多数时候我们的用户界面保持清晰。

    所有这些额外的工作都很重要:无论用户的可访问性需求如何,它都可以确保用户获得出色的体验,而这正是我们应一直追求的目标。

    用 Timer 倒计时

    如果我们将 Foundation,SwiftUI 和 Combine相结合,则可以向应用程序添加计时器,以给用户带来一点压力。一个简单的实现不需要太多的工作,但是它也有一个错误,需要一些额外的工作来修复。

    对于计时器的第一次传递,我们将创建两个新属性:计时器本身,它将每秒触发一次;以及timeRemaining属性,我们将在每次触发计时器时从中减去1。这将使我们能够显示当前应用程序运行中还剩下多少秒,这应该会给用户带来加速的积极动力。

    因此,首先将以下两个新属性添加到ContentView

    @State private var timeRemaining = 100
    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    

    这使用户有100秒的开始时间,然后创建并启动一个计时器,该计时器在主线程上每秒触发一次。

    每当计时器启动时,我们都想从timeRemaining中减去1,以便倒计时。我们可以在这里尝试一些日期数学,方法是存储开始日期并显示该日期与当前日期之间的差额,但是您确实会发现确实没有必要!

    将此onReceive()修饰符添加到ContentView中最外面的ZStack中:

    .onReceive(timer) { time in
        if self.timeRemaining > 0 {
            self.timeRemaining -= 1
        }
    }
    

    提示:这增加了一个很小的条件,以确保我们永远不会录入负数。

    该代码使我们的计时器从100开始,使其倒数至0,但实际上需要显示它。这就像在我们的布局中添加另一个文本视图一样简单,这次使用深色背景色来确保它清晰可见。

    将其放入和包含卡片的ZStack的同一个VStack中:

    Text("Time: \(timeRemaining)")
        .font(.largeTitle)
        .foregroundColor(.white)
        .padding(.horizontal, 20)
        .padding(.vertical, 5)
        .background(
            Capsule()
                .fill(Color.black)
                .opacity(0.75)
        )
    

    如果你代码写对了,那么应该是这样的预览效果:


    您应该可以立即运行该应用并尝试一下——它运行良好,对吗?嗯,有一个小问题:

    1. 看一下计时器中的当前值。
    2. 按Cmd + H返回主屏幕。
    3. 等待大约十秒钟。
    4. 现在点击您应用的图标以返回到该应用。
    5. 计时器显示什么时间?

    我发现计时器显示的值比以前在应用程序中时的值低约三秒钟——计时器在后台运行几秒钟,然后暂停直到应用程序返回。

    我们可以做得更好:我们可以检测到应用何时移至后台或前台,然后暂停并适当地重启计时器。

    首先,添加此属性以存储该应用程序当前是否处于活动状态:

    @State private var isActive = true
    

    接下来,我们需要在前一个onReceive()修饰符下方添加两个onReceive()修饰符,以在应用程序往返于后台时操纵isActive。对于这些,我们可以捕获UIApplication.willResignActiveNotificationUIApplication.willEnterForegroundNotification通知,如下所示:

    .onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { _ in
        self.isActive = false
    }
    .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
        self.isActive = true
    }
    

    最后,修改onReceive(timer)函数,以便isActive为 false 时立即退出,如下所示:

    .onReceive(timer) { time in
        guard self.isActive else { return }
        if self.timeRemaining > 0 {
            self.timeRemaining -= 1
        }
    }
    

    有了很小的改动,计时器就会在应用程序移至后台时自动暂停——我们不再失去任何神秘的秒数。

    思考:这样做真的好吗?

    allowHitTesting()结束应用

    SwiftUI通过将allowHitTesting()设置为false来禁用视图的交互性,因此在我们的项目中,当时间用完时,我们可以使用它通过检查timeRemaining的值来禁用任何卡上的滑动操作。

    首先将此修饰符添加到最里面的ZStack——显示我们的卡片堆栈的那个:

    .allowsHitTesting(timeRemaining > 0)
    

    timeRemaining为1或更大时,这将启用点击处理,但是如果用户没有时间,则将其设置为 false。

    另一个结果是,用户正确地划过所有卡,最后一无所有。当最后一张卡消失时,现在我们的计时器向下滑动到屏幕中央,并继续滴答作响。我们想要发生的是使计时器停止运行,以便用户可以看到自己的运行速度,并显示一个按钮,允许他们重置卡并重试。

    这需要一些思考,因为仅将isActive设置为 false 是不够的——如果应用程序移至后台并返回,即使没有剩余卡,isActive也将重新变成 true。

    让我们逐步解决它。首先,我们需要一种方法来重置应用程序,以便用户可以重试,因此请将其添加到ContentView中:

    func resetCards() {
        cards = [Card](repeating: Card.example, count: 10)
        timeRemaining = 100
        isActive = true
    }
    

    其次,我们需要一个按钮来触发它,仅在所有卡都被移除后才显示。将其放在最里面的ZStack之后,在allowHitTesting()修饰符下面:

    if cards.isEmpty {
        Button("Start Again", action: resetCards)
            .padding()
            .background(Color.white)
            .foregroundColor(.black)
            .clipShape(Capsule())
    }
    

    现在我们有了代码来重置卡时重新启动计时器,但是现在我们需要在移除最后一张卡时停止计时器——并确保回到前台时它保持停止状态。

    我们可以通过将这段代码添加到removeCard(at:)方法的末尾来解决第一个问题:

    if cards.isEmpty {
        isActive = false
    }
    

    至于第二个问题——确保isActive从后台返回时保持 false —— 我们应该只更新附加到willEnterForegroundNotification的函数,以便它显式检查卡片数量:

    .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
        if self.cards.isEmpty == false {
            self.isActive = true
        }
    }
    

    运行试一下吧!

    译自
    Coloring views as we swipe
    Counting down with a Timer
    Ending the app with allowsHitTesting()

    相关文章

      网友评论

        本文标题:Hacking with iOS: SwiftUI Editio

        本文链接:https://www.haomeiwen.com/subject/feslmktx.html