[译]如何使用Swift UI构建新形态设计

174 阅读18分钟

原文链接:How to build neumorphic designs with SwiftUI

尝试新的设计趋势,学习新的东西

新形态设计无疑是最近几个月最有趣的设计趋势,尽管公平地说,苹果早在WWDC 18就把它作为他们的设计主题。在本文中,我们将研究如何使用Swift UI构建新形态设计,为什么您可能想要,以及最重要的是,我们如何修改这些设计以使其更易于访问。

重要提示:新形态——有时被称为新形态——对可访问性有严重的影响,所以尽管阅读本文的第一部分然后跳过其余部分很有诱惑力,但我鼓励你留下来,了解缺点和优点,这样你就能全面了解情况。

赞助与Swift黑客,并达到世界上最大的Swift社区!

新形态的基础

在我们进入代码之前,我只想简单地谈谈这个设计趋势的两个核心原则,因为它将随着我们的进展而相关:

  1. 它使用高光和阴影来定义屏幕上对象的形状。

  2. 对比度通常会降低;不使用全白色或黑色,这使得高光和阴影脱颖而出。

最终结果是一种“挤压塑料”外观——一种用户界面设计,看起来确实新鲜有趣,但不会对你的眼睛造成伤害。我再怎么重复也不为过,减少对比度和依赖阴影来塑造形状会对可访问性产生严重影响,我们稍后会回到这一点。

然而,我仍然认为花时间探索Swift UI中的新形态是值得的——即使你不在自己的应用程序中使用它,它也有点像编码卡塔,有助于磨练你的技能。

好吧,华夫饼够了——让我们看看一些代码。

构建新形态卡

最简单的出发点是构建一个纽莫图卡:一个圆角矩形,它将包含一些信息。从那里,我们将研究如何将其转移到Swift UI的其他部分,但原则保持不变。

首先使用单视图应用模板创建一个新的ios项目。确保用户界面使用Swift UI,然后将其命名为新形态。

**提示:**如果您可以访问Xcode的Swift UI预览,我建议您现在就激活它——您会发现实验要容易得多。

我们将从定义一种颜色来代表灰白色开始。这不是灰色,而是一种非常微妙的色调,给用户界面增添了一点温暖或凉爽。如果你愿意,你可以在资产目录中添加它,但是在这里代码更容易。

在Content View结构之外添加此Color扩展:

extension Color {    static let offWhite = Color(red: 225 / 255, green: 225 / 255, blue: 235 / 255)}

是的,那几乎是白色的,但是它足够暗,当我们需要的时候,真正的白色会像辉光一样突出。

现在我们可以填充Content View的正文,给它一个占据整个屏幕的ZStack,并使用我们新的灰白色来填充整个空间:

struct ContentView: View {    var body: some View {        ZStack {            Color.offWhite        }        .edgesIgnoringSafeArea(.all)    }}

我们将使用一个圆角矩形来表示我们的卡片,我们将把它固定在300 x300,使它在屏幕上清晰可见。所以,将此添加到Z Stack,在颜色下面:

RoundedRectangle(cornerRadius: 25)    .frame(width: 300, height: 300)

这将有一个默认的黑色,但是在新形态中,我们希望大大降低对比度,所以我们用我们用于背景的相同颜色来代替它——有效地使形状看不见。

所以,把它改成这样:

RoundedRectangle(cornerRadius: 25)    .fill(Color.offWhite)    .frame(width: 300, height: 300)

现在是重要的部分:我们使用阴影来定义形状,一个黑暗和一个光明,就好像有一个光线从屏幕的左上角投射出来。

Swift UI允许我们多次应用修饰符,这使得新形态很容易实现。将这两个修饰符添加到圆角矩形中,以查看它的作用:

.shadow(color: Color.black.opacity(0.2), radius: 10, x: 10, y: 10).shadow(color: Color.white.opacity(0.7), radius: 10, x: -5, y: -5)

右下是一个深色阴影偏移,左上角是一个浅色阴影偏移。光影是可见的,因为我们使用灰白色作为背景,卡片一起变得可见。

我们只写了几行代码,但我们已经有了一个新形态卡——我希望你会同意Swift UI让它变得异常简单!

构建一个简单的纽带按钮

就用户界面元素而言,卡片出现新形态的风险相当低——只要卡片内部的用户界面是清晰的,卡片边框本身很容易不存在,也不会影响可访问性。按钮是另一回事,因为它们的存在是为了被窃听,所以降低它们的对比度弊大于利。

让我们通过创建自定义按钮样式来探索这一点,这就是Swift UI如何让我们在许多地方共享按钮配置。这比在我们创建的每个按钮上附加许多修饰符要方便得多——我们只需定义一次样式,并在许多地方共享它。

我们将定义一个实际上是空的按钮样式: Swift UI会给我们按钮的标签,可能是一些文本、图像或其他东西,我们会不加修改地将其发回。

在Content View之外添加此结构:

struct SimpleButtonStyle: ButtonStyle {    func makeBody(configuration: Self.Configuration) -> some View {        configuration.label    }}

这configuration.label就是按钮的内容,我们很快会添加更多内容。不过,首先,让我们定义一个使用它的按钮,这样你就可以看到设计的发展:

Button(action: {    print("Button tapped")}) {    Image(systemName: "heart.fill")        .foregroundColor(.gray)}.buttonStyle(SimpleButtonStyle())

你不会在屏幕上看到任何特别的东西,但是我们可以通过在按钮样式中添加我们的新形态效果来解决这个问题。这一次我们不会使用圆角矩形,因为对于简单的图标来说,圆形看起来更好,但是我们确实需要添加一些填充,这样按钮的点击区域就又好又大。

修改make Body()方法,以便我们添加一些填充,然后将我们的neumorpic效果作为按钮的背景:

configuration.label    .padding(30)    .background(        Circle()            .fill(Color.offWhite)            .shadow(color: Color.black.opacity(0.2), radius: 10, x: 10, y: 10)            .shadow(color: Color.white.opacity(0.7), radius: 10, x: -5, y: -5)    )

这让我们在很大程度上达到了我们想要的效果,但是如果你运行这个应用程序,你会发现它在实践中表现不佳——按钮按下时没有视觉反应,这很奇怪

为了解决这个问题,我们需要阅读自定义按钮样式中的配置. is Pared属性,该属性报告按钮当前是否被按住。我们可以用它来调整我们的样式,以给出按钮是否被按下的一些视觉指示。

让我们从简单的开始:我们将使用一个组作为按钮的背景,然后检查配置。如果按钮被按下,请返回一个平面圆圈,或者返回我们当前的阴影圆圈,否则:

configuration.label    .padding(30)    .background(        Group {            if configuration.isPressed {                Circle()                    .fill(Color.offWhite)            } else {                Circle()                    .fill(Color.offWhite)                    .shadow(color: Color.black.opacity(0.2), radius: 10, x: 10, y: 10)                    .shadow(color: Color.white.opacity(0.7), radius: 10, x: -5, y: -5)            }        }    )

因为is Pred状态使用了一个白色的圆圈,所以当按住按钮时,它会使我们的效果不可见。

**警告:**由于Swift UI计算可点击区域的方式,我们只是意外地将按钮的可点击区域做得非常小——您需要点击图像本身,而不是它周围的新形态设计。要解决这个问题,请直接在. padd in(30)之后添加. content Shape(Circle())修饰符,强制Swift UI使用所有可用空间进行点击。

现在,我们可以通过翻转阴影来创建一个假的凹陷效果——从常规效果中复制两个阴影修饰符,然后交换白色和黑色的X和Y值,像这样:

if configuration.isPressed {    Circle()        .fill(Color.offWhite)        .shadow(color: Color.black.opacity(0.2), radius: 10, x: -5, y: -5)        .shadow(color: Color.white.opacity(0.7), radius: 10, x: 10, y: 10)} else {    Circle()        .fill(Color.offWhite)        .shadow(color: Color.black.opacity(0.2), radius: 10, x: 10, y: 10)        .shadow(color: Color.white.opacity(0.7), radius: 10, x: -5, y: -5)}

试试看,看看你的想法!

为按钮按钮创建内部阴影

我们目前的代码有点奏效,但人们对效果的解释不同——有些人认为它是一个凹形按钮,另一些人认为它是按钮仍在升起,但光线来自不同的角度。

一个更好的想法是创建一个内部阴影,看起来更像是按钮被向内按下。这不是以Swift UI为标准的,但是我们可以很容易地制作一个。

创建内部阴影需要两个线性渐变,这将是我们将在本文中使用的许多内部渐变中的第一个,因此我们将向线性渐变添加一个小助手扩展,以使创建标准渐变更加容易:

extension LinearGradient {    init(_ colors: Color...) {        self.init(gradient: Gradient(colors: colors), startPoint: .topLeading, endPoint: .bottomTrailing)    }}

有了它,我们可以提供一个可变的颜色列表,以获得它们在对角线方向上的线性梯度。

现在是重要的部分:我们不会在我们按下的圆圈上添加两个翻转的阴影,而是在它上面覆盖一个新圆圈,给它一个模糊的笔画,然后用另一个有渐变的圆圈遮蔽它。这很多,但让我分解一下:

  • 我们的基圆是我们现在拥有的新形态效应圆,它充满了我们的灰白色。

  • 我们在上面放了一个圆圈,用灰色边框划了一下,模糊了一点,使它成为一个柔软的边缘。

  • 然后,我们用另一个圆来掩盖这个重叠的圆,这次用线性梯度填充。

当您用另一个视图遮罩一个视图时,Swift UI使用遮罩的alpha通道来确定底层视图应该显示什么。因此,如果我们画一个模糊的灰色笔画,然后用黑色的线性渐变来遮掩它,模糊的笔画将在一边看不见,在另一边淡入——我们得到一个平滑的内部渐变。为了使效果更加明显,我们可以在任何方向上稍微抵消笔画的圆圈,通过一点实验,我发现用比黑影更粗的线条绘制光影确实有助于最大化效果。

记住,新形态使用两个阴影,一个浅色和一个深色,来创造深度感,所以我们将用不同的颜色添加两次这种内部阴影效果。

修改配置. is Pared圆圈如下:

Circle()    .fill(Color.offWhite)    .overlay(        Circle()            .stroke(Color.gray, lineWidth: 4)            .blur(radius: 4)            .offset(x: 2, y: 2)            .mask(Circle().fill(LinearGradient(Color.black, Color.clear)))    )    .overlay(        Circle()            .stroke(Color.white, lineWidth: 8)            .blur(radius: 4)            .offset(x: -2, y: -2)            .mask(Circle().fill(LinearGradient(Color.clear, Color.black)))    )

如果你再次运行这个应用程序,你会看到按钮按下效果更加明显,看起来也更好。

走向黑暗

在我们开始研究如何改善新形态的可访问性问题之前,让我们看看如何利用这种效果来创造其他有趣的风格。

首先,在Color扩展中再添加两种颜色,这样我们就可以使用一些常量值:

static let darkStart = Color(red: 50 / 255, green: 60 / 255, blue: 65 / 255)static let darkEnd = Color(red: 25 / 255, green: 25 / 255, blue: 30 / 255)

我们现在可以用它作为Content View的背景,替换它现有的Color.white,如下所示:

var body: some View {    ZStack {        LinearGradient(Color.darkStart, Color.darkEnd)

我们的Simple Button Style现在看起来不合适,因为它在黑暗背景上应用了明亮的样式。所以,我们将创建一个新的更好的黑暗风格,但是这次我们将把它分成两部分:一个我们可以在任何地方应用的背景视图,以及一个将它与填充和内容形状修饰符一起包装的按钮风格。正如你所看到的,这将使我们更加灵活。

我们将要添加的新背景视图将允许我们为视觉效果指定任何形状,所以我们不再仅仅局限于圆形。它还将跟踪是否绘制凹形我们的凸效果(在或出),这取决于我们可以发送到外部的is Highlight属性。

我们将从非常简单的开始,使用修改后的阴影翻转方法来获得新闻效果。现在添加此结构:

struct DarkBackground<S: Shape>: View {    var isHighlighted: Bool    var shape: S    var body: some View {        ZStack {            if isHighlighted {                shape                    .fill(Color.darkEnd)                    .shadow(color: Color.darkStart, radius: 10, x: 5, y: 5)                    .shadow(color: Color.darkEnd, radius: 10, x: -5, y: -5)            } else {                shape                    .fill(Color.darkEnd)                    .shadow(color: Color.darkStart, radius: 10, x: -10, y: -10)                    .shadow(color: Color.darkEnd, radius: 10, x: 10, y: 10)            }        }    }}

那里的修改是,当按钮按下时,阴影尺寸会减小——我们使用5点的距离,而不是10点。

然后我们可以把它包装在一个暗按钮样式中,它应用填充和内容形状,像这样:

struct DarkButtonStyle: ButtonStyle {    func makeBody(configuration: Self.Configuration) -> some View {        configuration.label            .padding(30)            .contentShape(Circle())            .background(                DarkBackground(isHighlighted: configuration.isPressed, shape: Circle())            )    }}

最后,我们可以在Content View中通过更改按钮Style()修饰符来激活它:

.buttonStyle(DarkButtonStyle())

试试看,看看你是怎么想的——尽管我们没有很多代码,但我认为结果看起来相当不错。

一点实验

现在是尝试这种效果的好时机,因为它将帮助您准确理解Swift UI为我们做了什么。

例如,我们可以通过向按钮添加线性渐变来创建一个平滑的凹凸状按钮,并在按下时翻转它:

if isHighlighted {    shape        .fill(LinearGradient(Color.darkEnd, Color.darkStart))        .shadow(color: Color.darkStart, radius: 10, x: 5, y: 5)        .shadow(color: Color.darkEnd, radius: 10, x: -5, y: -5)} else {    shape        .fill(LinearGradient(Color.darkStart, Color.darkEnd))        .shadow(color: Color.darkStart, radius: 10, x: -10, y: -10)        .shadow(color: Color.darkEnd, radius: 10, x: 10, y: 10)}

如果你尝试一下,你会看到按钮在你按下和释放它时平稳地上下动画。我发现动画有点分散注意力,所以我建议您通过在现有的background()修饰符之后添加此修饰符来禁用它:

.animation(nil)

这种“枕头”按钮效果很迷人,但是如果你正在考虑使用它,我建议你尝试三个变化来帮助按钮更加突出。

首先,即使它违背了新形态设计的低对比度原则,我还是会抛弃灰色图标,转而使用白色,这样它才真正脱颖而出。所以,在Content View中,你会使用这个:

Image(systemName: "heart.fill")    .foregroundColor(.white)

其次,如果您在按钮的按压状态下添加一个覆盖图,它不仅看起来更像一个被按压的物理按钮,而且还有助于区分其按压状态和未按压状态。

要做到这一点,您需要在填充()之后插入一个overlay()修饰符,当is Highlight为true时,如下所示:

if isHighlighted {    shape        .fill(LinearGradient(Color.darkEnd, Color.darkStart))        .overlay(shape.stroke(LinearGradient(Color.darkStart, Color.darkEnd), lineWidth: 4))        .shadow(color: Color.darkStart, radius: 10, x: 5, y: 5)        .shadow(color: Color.darkEnd, radius: 10, x: -5, y: -5)

为了获得更清晰的外观,您可以删除按下状态的两个阴影()修饰符,这实际上聚焦了覆盖层。

第三,你也可以在按下的状态下添加一个覆盖图,只是为了帮助标记它是一个按钮。把它直接放在填充()后面,就像这样:

} else {    shape        .fill(LinearGradient(Color.darkStart, Color.darkEnd))        .overlay(shape.stroke(Color.darkEnd, lineWidth: 4))        .shadow(color: Color.darkStart, radius: 10, x: -10, y: -10)        .shadow(color: Color.darkEnd, radius: 10, x: 10, y: 10)}

添加切换样式

将按钮样式与新形态背景样式分开的好处之一是,我们现在可以使用相同的效果添加切换样式。这意味着创建一个新的结构,该结构符合To ggle Style协议,它类似于Button Style,除了:

  1. 我们应该阅读配置. is On以确定是否启用切换。

  2. 我们需要提供一个按钮来处理实际的切换动作本身,或者至少是某种on TapGesture()或类似的。

现在将此结构添加到您的项目:

struct DarkToggleStyle: ToggleStyle {    func makeBody(configuration: Self.Configuration) -> some View {        Button(action: {            configuration.isOn.toggle()        }) {            configuration.label                .padding(30)                .contentShape(Circle())        }        .background(            DarkBackground(isHighlighted: configuration.isOn, shape: Circle())        )    }}

我们想把其中一个放在Content View中,这样你就可以自己尝试了,所以首先在那里添加这个属性:

@State private var isToggled = false

接下来,将现有按钮包装在一个间隔为40的VS tack中,并将其放置在下面:

Toggle(isOn: $isToggled) {    Image(systemName: "heart.fill")        .foregroundColor(.white)}.toggleStyle(DarkToggleStyle()) 

您的Content View结构应该如下所示:

struct ContentView: View {    @State private var isToggled = false    var body: some View {        ZStack {            LinearGradient(Color.darkStart, Color.darkEnd)            VStack(spacing: 40) {                Button(action: {                    print("Button tapped")                }) {                    Image(systemName: "heart.fill")                        .foregroundColor(.white)                }                .buttonStyle(DarkButtonStyle())                Toggle(isOn: $isToggled) {                    Image(systemName: "heart.fill")                        .foregroundColor(.white)                }                .toggleStyle(DarkToggleStyle())            }        }        .edgesIgnoringSafeArea(.all)    }}

就这样——我们在两个地方分享了我们共同的肿瘤设计!

修复可访问性

我们有足够的时间来研究各种新形态风格,但现在我想直面它的问题:对比度的极度缺乏意味着按钮和其他重要控件无法从它们周围的东西中脱颖而出,这使得视力受损的人更难使用我们的应用程序。

在这一点上,我看到一些困惑在盘旋,所以我想先说几件事:

  1. 是的,我意识到苹果的默认按钮看起来只是蓝色文本,所以至少一开始“看起来”不像按钮,但是它们的对比度确实很高。

  2. 仅仅说“它对我有用,但我可以添加一个特殊的选项来让它更容易访问”是不够的——可访问性不是“好东西”,而是一种要求,所以我们的应用应该默认是可访问的,而不是“选择加入可访问的”。

  3. 我们制作的控件仍然是Swift UI按钮和切换,这意味着我们所有的更改都没有影响VoiceOver或苹果其他辅助技术的可见性或功能。

您已经看到了我们如何将灰色图标切换为白色,以立即增强对比度,但是按钮和切换仍然需要更多的对比度,如果它们想要被访问的话。

因此,我们将探索我们可以做出哪些改变来帮助事情真正脱颖而出。

首先,我想让你给我们的扩展添加两种新颜色:

static let lightStart = Color(red: 60 / 255, green: 160 / 255, blue: 240 / 255)static let lightEnd = Color(red: 30 / 255, green: 80 / 255, blue: 120 / 255)

其次,复制你现有的黑暗背景,并调用副本彩色背景。我们稍后会编辑这个,但是我们需要先完成其余的设置。

第三,复制暗按钮样式和切换样式,将它们重命名为Color Button Style和Color To ggle Style,然后使它们使用新的Color background作为背景。

所以,它们应该是这样的:

struct ColorfulButtonStyle: ButtonStyle {    func makeBody(configuration: Self.Configuration) -> some View {        configuration.label            .padding(30)            .contentShape(Circle())            .background(                ColorfulBackground(isHighlighted: configuration.isPressed, shape: Circle())            )            .animation(nil)    }}struct ColorfulToggleStyle: ToggleStyle {    func makeBody(configuration: Self.Configuration) -> some View {        Button(action: {            configuration.isOn.toggle()        }) {            configuration.label                .padding(30)                .contentShape(Circle())        }        .background(            ColorfulBackground(isHighlighted: configuration.isOn, shape: Circle())        )    }}

最后,编辑按钮并在Content View中切换以使用新的按钮样式:

Button(action: {    print("Button tapped")}) {    Image(systemName: "heart.fill")        .foregroundColor(.white)}.buttonStyle(ColorfulButtonStyle())Toggle(isOn: $isToggled) {    Image(systemName: "heart.fill")        .foregroundColor(.white)}.toggleStyle(ColorfulToggleStyle())

如果你愿意,你可以运行这个应用,但没什么意义——它实际上并没有改变。

为了让我们丰富多彩的版本栩栩如生,我们将为按下和未按下状态更改填充()和覆盖()修饰符。所以,当is Highlight为true时,将黑暗开始和黑暗结束更改为光开始和光结束,像这样:

if isHighlighted {    shape        .fill(LinearGradient(Color.lightEnd, Color.lightStart))        .overlay(shape.stroke(LinearGradient(Color.lightStart, Color.lightEnd), lineWidth: 4))        .shadow(color: Color.darkStart, radius: 10, x: 5, y: 5)        .shadow(color: Color.darkEnd, radius: 10, x: -5, y: -5)

如果你再次运行这个应用程序,你会发现它已经有了很大的改进:按下的状态现在有了强烈的蓝色,所以当按钮关闭或切换激活时,它是清晰的。但是我们仍然可以做得更好——当按钮没有按下时,我们可以在按钮周围添加相同的颜色,帮助吸引注意力。

为此,请将未按下状态的现有Overlay()更改为:

.overlay(shape.stroke(LinearGradient(Color.lightStart, Color.lightEnd), lineWidth: 4))

所以,完成的按钮样式应该是这样的:

ZStack {    if isHighlighted {        shape            .fill(LinearGradient(Color.lightEnd, Color.lightStart))            .overlay(shape.stroke(LinearGradient(Color.lightStart, Color.lightEnd), lineWidth: 4))            .shadow(color: Color.darkStart, radius: 10, x: 5, y: 5)            .shadow(color: Color.darkEnd, radius: 10, x: -5, y: -5)    } else {        shape            .fill(LinearGradient(Color.darkStart, Color.darkEnd))            .overlay(shape.stroke(LinearGradient(Color.lightStart, Color.lightEnd), lineWidth: 4))            .shadow(color: Color.darkStart, radius: 10, x: -10, y: -10)            .shadow(color: Color.darkEnd, radius: 10, x: 10, y: 10)    }}

现在再次运行这个应用程序,你会看到按钮和切换周围有一个蓝色的环,按下时它会填满蓝色——清晰得多。

包起来

实际上,你不会在同一个项目中有多个不同的按钮样式,或者至少如果你不喜欢让用户头疼的话。但是实验很有趣,我希望在这篇文章中能遇到这一点,因为你可以不用做很多工作就能创造出一些非常漂亮的效果。

我已经反复说过,你应该时刻关注你的可访问性,这意味着不仅仅是确保画外音与你的用户界面配合使用。确保你的按钮起来是互动的,确保你的文本标签和图标与其背景有足够的对比度(至少4.5:1,但目标是7:1),并确保你的点击区域漂亮而大(至少44 x44点)。

所以,无论如何,使用新形态设计来进行实验,并用它来帮助增加你对Swift UI的了解,但永远不要忘记,如果你为了一个奇特的新设计趋势而牺牲可用性,没有人会赢。

获取此项目的完整源代码:****从GitHub下载

赞助与Swift黑客,并达到世界上最大的Swift社区!