Hacking with iOS: SwiftUI Edition

Hacking with iOS: SwiftUI Editio

2020-11-05  本文已影响0人  韦弦Zhy

\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()

上一篇下一篇

猜你喜欢

热点阅读