Practical programming · Use SwiftUI to complete an iOS note App from 0 to 1 (1)

project origin

Some time ago, the Rare Earth Nuggets client launched the " 闪念笔记" function, and as an early adopter, I experienced it a little bit.

The flash note interface is simple and easy to use. Compared with Evernote and Youdao Note 综合型笔记应用, the current flash note is currently focused on the notebook itself and is only used as a 知识储备recording tool.

After in-depth research, I found some surprises. The flash notes combined with the rare earth Nuggets traffic port, and Web端应用the browser 剪藏插件can form a closed-loop product ecosystem.

It is not difficult to guess that after the note-taking tools are fully functional and the usage rate covers a certain market share, the mobile and desktop applications will be formally created, cutting into the field of note-taking tools and cutting a bloody road.

Of course, the above are just conjectures.

Entering the topic, in this chapter we will build the first SwiftUIpractical project, and complete an iOS note-taking app from 0 to 1.

demand analysis

What is a minimal MVP note app like? Record your own thoughts and ideas in time, and search for the notes you have recorded. This is the most basic functional demand point of a note.

Let's sort it out first 产品架构图, as shown in the following figure:

1.png

One of the simplest note-taking software, including 2 core pages:

  • Home page interface: display note list, create new note button, if you want to enhance the function, you can add a search bar;
  • New button: Click the button to open the "New Page" pop-up window, or enter a new page, close/return to the home page after editing;

product design

In the next step, let's complete the product 原型设计, build 2 pages, and 元素信息convert the product architecture to 页面功能, as shown in the following figure:

2.png

When the user enters for the first time, Nianxiang Note will display a blank page. The blank page is composed of title, default image, guide text, and new note button, which mainly guides the user to use the main functions.

When the user clicks the new note button, the " 新建笔记" pop-up page will be invoked. Compared with the pop-up page, the pop-up page will enter a new page, and the user's thinking will not be forcibly interrupted. This is a very good interaction.

在“新建笔记”页面中,主要操作为“返回”按钮、“完成”按钮、标题输入框、内容输入框。

用户点击“返回”按钮,则向下关闭该弹窗,并清空该页面已输入内容;

当用户点击“完成”按钮时,需要判断“标题输入框”、“内容输入框”是否有键入的文字,且符合预设的规则(文字数量等);

当满足规则时,则吐司提示保存成功,并关闭该弹窗页面,且在笔记列表中插入一条新的笔记。若不满足规则(校验标题和内容是否为空),则吐司冒泡提示相关信息(标题不能为空、内容不能为空)。

为更好地指引用户填写,标题输入框需要有提示文字,当文字键入时,清空提示文字,内容输入框同理。

对于搜索功能,核心点在于根据内容关键字进行搜索,并实时进行反馈。当然通常由于性能问题,会考虑输入后点击“搜索”触发搜索功能的交互方式。

以上就是简单的需求文档的撰写。

UI设计

下面我们使用AdobeXD根据产品原型图绘制UI设计稿,UI设计稿需要包含所有的页面及其交互动作,如下图所示:

3.png

UI设计需要根据产品原型的元素和内容,结合当前市场上的常用交互逻辑和设计规范,输出高保真的设计稿。其中包含各项元素组件的尺寸规范、文字规范、交互规范等,旨在为前端开发人员提供很好的样式开发指引

UI设计师和产品经理的分工常常是产品经理提供产品原型DemoUI设计师确定产品的主体风格和设计规范,输出UI设计稿,再与产品经理或者与业务团队进行评审,通过后方可进行切图,移交给下一流程。

UI设计稿最为App最终呈现的效果产物,前端工程师需要根据UI设计稿达到一比一还原,后期也可以与UI设计稿进行对比进行App样式验收。

在这里科普一个概念,切图。

切图是UI设计师将已经完成好的静态页面中的元素使用工具进行切分,与静态页面分离,示例:图标按钮、插图。UI设计师将这些需要在实际开发过程中需要导入使用的素材从静态页面上“切”下来,便可以在下面的协同工具中导出不同尺寸的图片进行使用。

UI设计师切好图后,需要再借助一个工具交付给前端设计师,这里推荐的工具是PxCook像素大厨

安装完成,AdobeXD将会安装对应的插件,选择文件 > 导出 > PxCook。如下图所示:

4.png

PxCook将会被唤起,并创建一个新项目,输入“念头笔记”,选择类型为iOS,点击“创建本地项目”,如下图所示:

5.png

选择导入画板,PxCook会自动勾选由AdobeXD导出的所有面板,我们保持全选状态,点击“导入”,如下图所示:

6.png

如此,我们便将AdobeXD中的UI设计稿导入到PxCook中了。

在PxCook中,我们可以看到UI设计稿设计的元素的尺寸、元素之间的相对距离,方便于开发者根据UI设计稿进行前端静态页面的开发工作,如下图所示:

7.png

实战编程

接下来,我们正式进入到编程阶段,打开Xcode开发工具,点击Create a new Xcode project,将新项目命名为IdeaNote,如下弹窗所示:

8.png

首页-缺省图

点击视图工具栏的Assets.xcassets文件,拖入首页缺省图的图片,如下图所示:

9.png

回到ContentView文件,在用户初始进入时,映入眼帘的是一个初始的样式,它由图片文字组成,如下代码所示:

// 缺省图
func noDataView() -> some View {
    VStack(alignment: .center,spacing: 20) {
        Image("mainImage")
            .resizable()
            .scaledToFit()
            .frame(width: 240)
        Text("记录下这个世界的点滴")
            .font(.system(size: 17))
            .bold()
            .foregroundColor(.gray)
    }
}
复制代码

上述代码中,我们创建了一个新的缺省视图noDataView

图片与文字使用VStackc垂直布局容器,容器内的元素alignment对齐方式为center居中对齐,元素的spacing间距为20。

图片引用之前导入的mainImage图片,为了保持图片在视图内的展示效果,使用resizable修饰符调整图片大小,使用scaledToFit修饰符保持其宽高比,防止图片变形,最后使用frame修饰符设置图片的大小为240。

文字部分内容设置为“记录下这个世界的点滴”,使用font修饰符设置文字大小为17,使用bold修饰符使得文字加粗,使用foregroundColor修饰符设置文字填充颜色。

运行预览效果如下图所示:

10.png

新建笔记按钮样式也可以按照上述创建视图的方式,如下代码所示:

// 新建笔记按钮
func newBtnView() -> some View {
    VStack {
        Spacer()
        HStack {
            Spacer()
            Button(action: {

            }) {
                Image(systemName: "plus.circle.fill")
                    .font(.system(size: 48))
                    .foregroundColor(.blue)
            }
        }
    }
    .padding(.bottom, 32)
    .padding(.trailing, 32)
}
复制代码

上述代码中,我们创建了一个新的新建笔记按钮视图newBtnView

新建笔记按钮的核心是一个图标按钮,使用Image组件引用系统提供的图标“plus.circle.fill”,按钮的交互可以直接使用Button组件。

为了使按钮文字放置在屏幕右下角,我们使用VStack垂直容器和Spacer占位视图将按钮撑在底部,然后再使用HStack横向容器和Spacer占位视图再把按钮撑到右边,我们再使用padding在bottom底部和trailing右边都留出距离。

如此我们就完成新建笔记按钮的样式,最后我们在主视图中与noDataView缺省图使用ZStack层叠容器包裹,运行预览效果如下图所示:

11.png

还差点什么?标题。标题部分可以直接使用NavigationView导航视图与navigationTitle修饰符设置标题,如下代码所示:

NavigationView {
    ZStack {
        noDataView()
        newBtnView()
    }.navigationBarTitle("念头笔记", displayMode: .inline)
}
复制代码

12.png

首页-列表页

当用户新建了笔记之后,首页便从原本的缺省视图转换为列表视图,列表视图中的主要两块视图为“搜索栏”、“列表栏”,如下图所示:

13.png

我们至上而下构建页面,首先是搜索栏。从设计稿中,我们可以知道搜索栏由一个搜索图标、搜索输入框、清除按钮组成。由于会使用输入框TextField,因此需要提前声明绑定的变量,如下代码所示:

@State var searchText = ""
复制代码
// MARK: 搜索

func searchBarView() -> some View {
    TextField("搜索内容", text: $searchText)
        .padding(7)
        .padding(.horizontal, 25)
        .background(Color(.systemGray6))
        .cornerRadius(8)
        .overlay(
            HStack {
                Image(systemName: "magnifyingglass")
                    .foregroundColor(.gray)
                    .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
                    .padding(.leading, 8)

                // 编辑时显示清除按钮
                if searchText != "" {
                    Button(action: {
                        self.searchText = ""
                    }) {
                        Image(systemName: "multiply.circle.fill")
                            .foregroundColor(.gray)
                            .padding(.trailing, 8)
                    }
                }
            }
        )
        .padding(.horizontal, 10)
}
复制代码

上述代码中,我们创建了一个新的搜索栏视图searchBarView

搜索栏使用TextField输入框组件,内容text部分绑定声明好的变量searchText

样式部分使用padding修饰符撑开一段距离,使用background修饰符设置背景填充颜色为灰色,使用cornerRadius修饰符设置视图的圆角度数。

输入框右边有一个“搜索”的图标,这里使用overlay修饰符在输入框层叠一个“搜索”图标和一个“清除”图标按钮。

其中“清除”图标的交互逻辑是,判断输入框内输入的文字searchText是否为空,如果不为空,则展示“清除”按钮,当点击“清除”按钮的时候清空searchText内容。

运行预览效果如下图所示:

14.png

接下来是列表部分,依旧拆解下列表的元素,列表由记录时间、笔记标题、笔记内容、更多按钮组成,如下图所示:

15.png

拆解好元素后,由于列表不是固定写好的内容,而是由用户编辑输入的内容,因此我们需要构建数据模型。

在Xcode视图窗口右键,选择New File,创建一个新的Swift文件,名称为Model.swift。如下图所示:

16.png

创建完成后,录入以下代码:

import SwiftUI

class NoteItem: ObservableObject, Identifiable {
    var id = UUID()
    @Published var writeTime: String = ""
    @Published var title: String = ""
    @Published var content: String = ""

    // 实例化
    init(writeTime: String, title: String, content: String) {
        self.writeTime = writeTime
        self.title = title
        self.content = content
    }
}
复制代码

17.png

上述代码中,我们创建了一个类NoteItem,遵循ObservableObject可被观察对象协议和Identifiable可被识别协议。

在NoteItem类里面有三个参数:writeTime录入时间、title标题、content内容。并且在ObservableObject协议需要使用@Published定义,这样才能在参数改变的时候检测到变化。由于使用Identifiable可被识别协议,因此需要声明一个idUUID()

定义好Model数据模型后,回到ContentView文件,我们来创建列表视图,如下代码所示:

// MARK: 列表内容

struct NoteListRow: View {
    @ObservedObject var noteItem: NoteItem

    var body: some View {
        HStack {
            VStack(alignment: .leading, spacing: 10) {
                Text(noteItem.writeTime)
                    .font(.system(size: 14))
                    .foregroundColor(.gray)
                Text(noteItem.title)
                    .font(.system(size: 17))
                    .foregroundColor(.black)
                Text(noteItem.content)
                    .font(.system(size: 14))
                    .foregroundColor(.gray)
                    .lineLimit(1)
                    .multilineTextAlignment(.leading)
            }
            Spacer()

            Button(action: {

            }) {
                Image(systemName: "ellipsis")
                    .foregroundColor(.gray)
                    .font(.system(size: 23))
            }
        }
    }
}
复制代码

上述代码中,我们先创建了一个单条列表视图NoteListRow,使用@ObservedObject引用监听实例对象的类NoteItem

在构建列表视图的样式上,和基础构建视图的方式一致,只是原本固定录入的参数,变成了来自于NoteItem类的参数,示例:标题,使用noteItem.title

三个文本使用VStack纵向布局容器,设置左对齐以及间距为10。最后使用HStack横向布局容器包裹三个文本和“更多”按钮。由于noteItem.content内容文字可能很长,我们只需要一行,因此可以使用lineLimit限制长度为1行省略。

如此,便构建完成了单条笔记的样式。

然后我们基于单条笔记的样式构建列表视图,如下代码所示:

// MARK: 列表

struct NoteListView: View {
    @State var noteItems: [NoteItem] = [NoteItem(writeTime: "2022.09.17", title: "第一条笔记", content: "快来使用念头笔记记录生活吧~快来使用念头笔记记录生活吧~")]

    var body: some View {
        List {
            ForEach(noteItems) { noteItem in
                NoteListRow(noteItem: noteItem)
            }
        }
        .listStyle(InsetListStyle())
    }
}
复制代码

上述代码中,我们创建了一个NoteListView列表视图,使用@State声明一个数组noteItems,并赋予noteItems来源于NoteItem数组类并赋予内容。

在主体body部分,使用List列表组件和ForEach循环遍历noteItems数组的数据,并传递参数给NoteListRow

List列表样式部分,由于SwiftUI默认样式是圆角矩形分组的方式,这边还需要设置List列表样式为InsetListStyle

我们在ContentView的body中使用搜索栏视图和列表视图,如下代码所示:

NavigationView {
    ZStack {
        VStack {
            searchBarView()
            NoteListView()
        }
        newBtnView()
    }.navigationBarTitle("念头笔记", displayMode: .inline)
}
复制代码

运行预览效果如下图所示:

18.png

首页-页面判断

上述编程过程中,我们完成了缺省页和列表页,它们之间的交互逻辑是:当笔记列表中没有笔记时,App将展示缺省页,当存在笔记时,展示列表页。

可以先引入NoteItem数组类,如下代码所示:

@State var noteItems: [NoteItem] = [NoteItem(writeTime: "2022.09.17", title: "第一条笔记", content: "快来使用念头笔记记录生活吧~快来使用念头笔记记录生活吧~")]
复制代码

然后我们就可以根据noteItems数组的数量作为判断条件,如下代码所示:

NavigationView {
    ZStack {
        if noteItems.count == 0 {
            noDataView()
        } else {
            VStack {
                searchBarView()
                NoteListView()
            }
        }
        newBtnView()
    }.navigationBarTitle("念头笔记", displayMode: .inline)
}
复制代码

上述代码中,当noteItems数组中的数量为0时,则展示noDataView缺省页,否则则展示搜索栏+笔记列表组成的列表页。

运行预览效果如下图所示:

19.png

本章小结

由于项目较长,这里将分成几个章节完成,请按耐住性子一步一步完成。

在本章中,我们从产品规划开始,通过需求分析、产品设计、UI设计、实战编程等阶段来从0到1完成一款iOS笔记App,其中涉及到各个阶段不同职业的工作细节,希望能给大家对一款App全生命周期过程有一个大概的认识。

快来动手试试吧~

版权声明

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

おすすめ

転載: juejin.im/post/7144294305237532708