실용적인 프로그래밍 · SwiftUI를 사용하여 0에서 1까지 iOS 노트 앱 완성 (1)

프로젝트 출처

얼마전 Rare Earth Nuggets 클라이언트에서 " 闪念笔记" 기능을 런칭했는데, 얼리 어답터로서 조금 경험했습니다.

플래시 노트 인터페이스는 간단하고 사용하기 쉽습니다.Evernote 및 Youdao Note와 비교할 때 综合型笔记应用현재 플래시 노트는 현재 노트북 자체에 초점을 맞추고 있으며 知识储备녹음 도구로만 사용됩니다.

심도 있는 연구 끝에 몇 가지 놀라운 사실을 발견했습니다.플래시 노트와 희토류 너겟트 트래픽 포트가 결합되어 Web端应用브라우저 剪藏插件가 폐쇄형 루프 제품 생태계를 형성할 수 있습니다.

노트 작성 도구가 완전히 기능하고 사용률이 특정 시장 점유율을 커버하면 모바일 및 데스크톱 애플리케이션이 공식적으로 생성되어 노트 작성 도구 분야에 진입하고 피의 길을 끊을 것이라고 추측하는 것은 어렵지 않습니다. .

물론 위의 내용은 추측일 뿐입니다.

주제를 입력하면 이 장에서는 첫 번째 실용적인 프로젝트를 빌드하고 SwiftUI0에서 1까지 iOS 메모 작성 앱을 완성합니다.

수요 분석

최소한의 MVP 메모 앱은 어떤가요? 자신의 생각과 아이디어를 시간에 기록하고, 녹음한 노트를 검색하는 것이 노트의 가장 기본적인 기능 수요 포인트입니다.

产品架构图다음 그림과 같이 먼저 정렬해 보겠습니다 .

1.png

2개의 핵심 페이지를 포함하는 가장 간단한 메모 작성 소프트웨어 중 하나:

  • 홈페이지 인터페이스: 메모 목록 표시, 새 메모 만들기 버튼, 기능을 향상시키려는 경우 검색 표시줄을 추가할 수 있습니다.
  • 새 버튼: 버튼을 클릭하여 "새 페이지" 팝업 창을 열거나 새 페이지를 입력하거나 편집 후 홈 페이지를 닫거나 돌아갑니다.

제품 디자인

다음 단계에서는 다음 그림과 같이 제품을 완성하고 原型设计, 2페이지를 빌드하고 元素信息, 제품 아키텍처를 로 변환해 보겠습니다.页面功能

2.png

사용자가 처음 입력하면 Nianxiang Note는 빈 페이지를 표시하며, 빈 페이지는 주로 사용자가 주요 기능을 사용하도록 안내하는 제목, 기본 이미지, 안내 텍스트 및 새 노트 버튼으로 구성됩니다.

사용자가 새 노트 버튼을 클릭하면 " 新建笔记" 팝업 페이지가 호출됩니다. 팝업 페이지에 비해 팝업 페이지는 새 페이지로 들어가며 사용자의 생각이 강제로 중단되지 않습니다. 이것은 아주 좋은 상호 작용입니다.

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

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

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

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

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

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

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

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天后未获授权禁止转载,侵权必究!

Supongo que te gusta

Origin juejin.im/post/7144294305237532708
Recomendado
Clasificación