How to achieve a PDA APP development

Some time ago the implementation details of the account class opponents app is very interested in, then initiation would like to own a minimal realization of the feasibility of the product. Of course - since it is the product of MVP mode, so only achieve a "function", but in some places want to go their own special "plagiarism" is also under a bit of effort to pursue the performance of the UI.

Foreword

A child, I was a fan Shouchao Bao, fourth grade class organized a Shouchao Bao game, the teacher asked each student using the weekend to do a Shouchao Bao todetermine Theme Preferences. Now my impression is still very deep, I think a noon do not know what theme to choose, draw something on the white paper and then wipe all the soiled several sheets of paper, drawn up last Earth, thinking it slowly opened.

Monday to the teacher's time, and I can not pay first, I came in the team final. After the teacher received my Shouchao Bao, actually he said: "Come, come, you take a look at what Shouchao Bao", I was heart rate reached a high point, face red and hot, teacher standing beside station nor walk nor, awkward smile, but the heart is extremely proud.

To the junior high school, the teacher let everyone take advantage of weekend time to do a Shouchao Bao, because of little experience in elementary school, junior high school had never been seen together on to basically use a computer to assist a variety of tasks are also spread open, and I wondered innovation can no longer do. Kodak then came the collapse of the message, which is equivalent to a memory of a generation it - sometimes I'll go to the old house to turn all kinds of films, watching the reflected image is mapped out in the sunlight.

In conjunction with this event, I thought of using the "film" style to illustrate the theme of the protection of birds, some pictures downloaded from the Internet a variety of birds, their processing it, finally Shouchao Bao done to the teacher. When the moment to the teacher, the teacher pleasant smile, and took my Shouchao Bao to the students demonstrate on the podium, "Everybody look down, do good it ~ ah, very pretty!."

Finished college entrance examination that summer, "the Southern Metropolis Daily," organized a students Shouchao Bao contest, when I participate in this contest with the identity of the cousin, took third prize, the prize is a $ 500 textbook bookstore innovation card.

That is all I said to Shouchao Bao or similar hand-painted hand account of this experience, and I especially like the way a story can be good things I want to express through some text, pictures and paintings way to show it.

So, when there is a PDA class app, I quickly downloaded for use, the course has certainly achieved his original organization by some elements and text to tell one thing in mind. Some time ago thinking that if I could make myself a PDA, the way to explore a PDA app implementation issues that need attention, and that nice ah!

design

First of all, I searched the App Store under the "PDA" keyword ranking top 10 app have had some use, summed up some of the PDA app general points:

  • Add text. Can rotate, zoom, rotation font;

  • Add photos. Rotatable flip, zoom, and with simple tools or the auxiliary image modification;

  • Add stickers. Use some good drawing sticker, operation and "add photos" almost;

  • stencil. Providing a set of templates, the user can add content of prescribed region in the template;

  • Unlimited length or width of the canvas.

Basically these PDA app functionality is so much, because MVP in line with the idea to do this project, so it did not do high-fidelity design, direct copy of a relatively simple design of the PDA app.

体验过的手帐 app 集合(部分)


Technology stack

After determining a good point about the function needs to be done to achieve their own, we need to begin to choose the technology stack, because, after all, do not the MVP product demo, my understanding of the demo is "to achieve a certain function points" for understanding MVP product is "at a stage full of products available," something out of the MVP pattern detail at some problems not too blame, but the overall logic must be complete, incomplete logic can not, but once we should have a complete, logical path coverage may not be 100%, but the main logic must be full coverage.

Client

iOS APP develop technical points are as follows:

  • Pure native Swift development;

  • Network requests =>  Alamofire, some simple data directly go  NSFileManager for file persistence manager;

  • Based on all UI components  UIKit do; social sharing go system to share, not to integrate other SDK;

  • Provide "sticker" on the module, "brush", "Photo" and "text." Doing it discovered that in fact the "Photo" and "text" is the essence stickers, save a lot of things.

客户端架构


Server

其实我对自己每新开一个 side project 都有一个硬性要求,做完后要对自己的技术水平有增长,其实「增长」这个东西很玄学,怎么定义「增长」对吧?我给自己找到了一个最简单的思路:用新的东西去完成它!

因此在服务端上我就直接无脑的选择了 Vapor 进行,通过 Swift 去写服务端这是我之前一直想做但找不到时机去做的事情,借此机会就上车了。至于为什么不是选 Perfect,其实我个人没有去动手实践过,只是听大佬们说 Vapor 的 API 风格比较 Swifty 一些。

服务端架构


在第一期的 MVP 中对服务端的依赖不大,所以目前的架构比较简单,达到能用即可就完事了~关于 Vapor 的一些使用细节,可以在我的这篇文章中进行查看,本文将不再细述 Vapor 使用细节。

实现

手势

对于手帐来说,最核心的一个就是「贴纸」。如何把贴纸从存储中拉出来放到画布上,这一步解决了,后续大部分内容也都解决了。

首先,我们需要明确一点,在这个项目中,「画布」本身也是个 UIView,把「贴纸」添加到画布上,实质上就是把 UIImageView 给 addSubview 到 UIView 上。其次,手帐中追求的是对素材的控制,可旋转放大是基本操作,而且前文也说过了,我们几乎可以把「照片」和「文字」都认为是对「贴纸」的继承,所以这就抽离出了「贴纸」本身是所以可提供交互组件的基类。

手帐类 app 对贴纸进行多手势操作的流畅性是决定用户留存率很大的一个因素。因此,我们再抽离一下手帐「贴纸」,把基础手势操作都移到更高一层的父类中去,贴纸中留下业务逻辑。手势操作核心代码逻辑如下:

// pinchGesture 缩放手势// 缩放的方法(文件私有)。  gesture手势 :UI缩放手势识别器@objcfileprivate func pinchImage(gesture: UIPinchGestureRecognizer) {
    //  当前手势 状态   改变中
    if gesture.state == .changed {
        // 当前矩阵2D变换  缩放通过(手势缩放的参数)
        transform = transform.scaledBy(x: gesture.scale, y: gesture.scale)
        // 要复原到1(原尺寸),不要叠加放大
        gesture.scale = 1
    }}// rotateGesture 旋转手势// 旋转的方法(文件私有)。  gesture手势 :UI旋转手势识别器@objcfileprivate func rotateImage(gesture: UIRotationGestureRecognizer) {
    if gesture.state == .changed {
        transform = transform.rotated(by: gesture.rotation)
        // 0为弧度制(要跟角度转换)
        gesture.rotation = 0
    }}// panGesture 拖拽/平移手势// 平移的方法(文件私有)。  gesture手势 :UI平移手势识别器@objcfileprivate func panImage(gesture: UIPanGestureRecognizer) {
    if gesture.state == .changed {
        // 坐标转换至父视图坐标
        let gesturePosition = gesture.translation(in: superview)
        // 用移动距离与原位置坐标计算。 gesturePosition.x 已经带正负了
        center = CGPoint(x: center.x + gesturePosition.x, y: center.y + gesturePosition.y)
        // .zero 为 CGPoint(x: 0, y: 0)的简写, 位置坐标回0
        gesture.setTranslation(.zero, in: superview)
    }}// 双击动作(UI点击手势识别器)@objcfileprivate func doubleTapGesture(tap: UITapGestureRecognizer) {
    // 状态 双击结束后
    if tap.state == .ended {
        // 翻转 90度
        let ratation = CGFloat(Double.pi / 2.0)
        // 变换   旋转角度 = 之前的旋转角度 + 旋转
        transform = CGAffineTransform(rotationAngle: previousRotation + ratation)
        previousRotation += ratation    }}

实现的效果下图所示:

对贴纸增加的手势操作


使用 UICollectionView 作为贴纸容器,通过闭包把点击事件对应索引映射的 icon 图片实例化为贴纸对象传递给父视图:

collectionView.cellSelected = { cellIndex in
    let stickerImage = UIImage(named: collectionView.iconTitle + "\(cellIndex)")
    let sticker = UNStickerView()
    sticker.width = 100
    sticker.height = 100
    sticker.imgViewModel = UNStickerView.ImageStickerViewModel(image: stickerImage!)
    self.sticker?(sticker)}

在父视图中通过实现闭包接收贴纸对象,这样就完成了「贴纸」到「画布」的全流程。

stickerComponentView.sticker = {
    $0.viewDelegate = self
    // 父视图居中
    $0.center = self.view.center
    $0.tag = self.stickerTag    self.stickerTag += 1
    self.view.addSubview($0)
    // 添加到贴纸集合中
    self.stickerViews.append($0)}

「照片」和「文字」

手帐编辑页面的底部工具栏之前没做好设计,按道理来说,应该直接上一个 UITabBar 即可完事,但最终也使用了 UICollectionView 完成。读取设备照片操作比较简单,不需要自定义相册,所以通过系统的 UIImagePicker 完成,对自定义相册感兴趣的同学可以看我的这篇文章。顶部工具栏的代码细节如下所示:

// 底部的点击事件collectionView.cellSelected = { cellIndex inswitch cellIndex {
    // 背景
    case 0:
        self.stickerComponentView.isHidden = true

        brushView.isHidden = true
        self.bgImageView.image = brushView.drawImage()

        self.present(self.colorBottomView, animated: true, completion: nil)
    // 贴纸
    case 1:
        brushView.isHidden = true
        self.bgImageView.image = brushView.drawImage()

        self.stickerComponentView.isHidden = false
        UIView.animate(withDuration: 0.25, animations: {
            self.stickerComponentView.bottom = self.bottomCollectionView!.y        })
    // 文字
    case 2:
        self.stickerComponentView.isHidden = true

        brushView.isHidden = true
        self.bgImageView.image = brushView.drawImage()

        let vc = UNTextViewController()
        self.present(vc, animated: true, completion: nil)
        vc.complateHandler = { viewModel in
            let stickerLabel = UNStickerView(frame: CGRect(x: 150, y: 150, width: 100, height: 100))
            self.view.addSubview(stickerLabel)
            stickerLabel.textViewModel = viewModel            self.stickerViews.append(stickerLabel)
        }
    // 照片
    case 3:
        self.stickerComponentView.isHidden = true

        brushView.isHidden = true
        self.bgImageView.image = brushView.drawImage()

        self.imagePicker.delegate = self
        self.imagePicker.sourceType = .photoLibrary        self.imagePicker.allowsEditing = true
        self.present(self.imagePicker, animated: true, completion: nil)
    // 画笔
    case 4:
        self.stickerComponentView.isHidden = true

        brushView.isHidden = false
        self.bgImageView.image = nil
        self.view.bringSubviewToFront(brushView)
    default: break}

底部工具栏的每一个模块都是一个 UIView,这部分做的也不太好,最佳的做法应该是基于 UIWindow 或者 UIViewController 做一个「工具容器」作为各个模块 UI 内容元素的容器,通过这种做法就可以免去在底部工具栏的点击事件回调中写这么多的视图显示 / 隐藏的状态代码。

关注「照片」部分的代码块,实现 UIImagePickerControllerDelegate 协议后的方法为:

extension UNContentViewController: UIImagePickerControllerDelegate {
    /// 从图片选择器中获取选择到的图片
    func imagePickerController(_ picker: UIImagePickerController,
                               didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        // 获取到编辑后的图片
        let image = info[UIImagePickerController.InfoKey.editedImage] as? UIImage
        if image != nil {
            let wh = image!.size.width / image!.size.height            // 初始化贴纸
            let sticker = UNStickerView(frame: CGRect(x: 150, y: 150, width: 100, height: 100 * wh))
            // 添加视图
            self.view.addSubview(sticker)
            sticker.imgViewModel = UNStickerView.ImageStickerViewModel(image: image!)
            // 添加到贴纸集合中
            self.stickerViews.append(sticker)

            picker.dismiss(animated: true, completion: nil)
        }
    }}

文字

文字模块暴露给父视图也是一个实例化后的贴纸对象,不过在文字 VC 里需要对文字进行颜色、字体和字号的选择。做完了才发现其实因为贴纸是可以通过手势进行放大和缩小的,没必要做字号的选择......

文字模块功能全览


其中比较费劲的是对文字颜色的选择,刚开始我想的直接上 RGB 调色就算了,后来想到如果直接通过 RGB 有三个通道,调起色来非常的难受。想到之前在做《疯狂弹球》这个游戏时使用的 HSB 颜色模式,做一个圆盘颜色选择器,后来在思考实现细节的过程中了这么 EF 写的这个库 EFColorPicker,非常好用,改了改 UI 后直接拿来用了,感谢 EF !

「气泡视图」的本身是个 UIViewController,但是需要对其几个属性进行设置。其实现流程比较流程化,比较好的做法是封装一下,把这些模版化的代码变成一个「气泡视图」类供业务方使用,但因为时间关系就一直在 copy,核心代码如下:

/// 文字大小气泡private var sizeBottomView: UNBottomSizeViewController {
    get {
        let sizePopover = UNBottomSizeViewController()
        sizePopover.size = self.textView.font?.pointSize
        sizePopover.preferredContentSize = CGSize(width: 200, height: 100)
        sizePopover.modalPresentationStyle = .popover        let sizePopoverPVC = sizePopover.popoverPresentationController
        sizePopoverPVC?.sourceView = self.bottomCollectionView
        sizePopoverPVC?.sourceRect = CGRect(x: bottomCollectionView!.cellCenterXs[1], y: 0, width: 0, height: 0)
        sizePopoverPVC?.permittedArrowDirections = .down
        sizePopoverPVC?.delegate = self
        sizePopoverPVC?.backgroundColor = .white

        sizePopover.sizeChange = { size in
            self.textView.font = UIFont(name: self.textView.font!.familyName, size: size)
        }

        return sizePopover    }}

在需要弹出该气泡视图的地方通过 present 即可调用:

collectionView.cellSelected = { cellIndex in
    switch cellIndex {
    case 0: self.present(self.fontBottomView,
                            animated: true,
                            completion: nil)
    case 1: self.present(self.sizeBottomView,
                            animated: true,
                            completion: nil)
    case 2: self.present(self.colorBottomView,
                            animated: true,
                            completion: nil)
    default: break
    }}

画笔

之前在滴滴实习时,写过一个关于画笔的组件(居然已经两年前了...),但是这个画笔是基于 drawRect: 方法去做的,对于内存十分不友好,一直画下去,内存就会一直涨,这回采用了 CAShapeLayer 重写了一个,效果还不错。

画笔


关于画笔的撤回之前基于 drawRect: 的方式去做就会非常简单,每一次的撤回相当于重绘一次,把被撤回的线从绘制点数组中 remove 掉就好了,但基于 CAShapeLayer 实现不太一样,因为其每一笔都是直接生成在 layer 中了,如果需要撤回就得把当前重新生成 layer

所以最后我的做法是每画一笔都去生成一张图片保存到数组中,当执行撤回操作时,就把撤回数组中的最后一个元素替换当前正在的绘制画布内容,并从撤回数组中移除这个元素。

有了撤回,那也要把重做给上了。重做的就是防止撤回,做法跟撤回类似。再创建一个重做数组,把每次从撤回数组中移除掉的图片都 append 到重做数组中即可。以下为撤回重做的核心代码:

// undo 撤回@objcprivate func undo() {
    // undoDatas 可撤回集合 数量
    guard undoDatas.count != 0 else { return }

    // 如果是撤回集合中只有 1 个数据,则说明撤回后为空
    if undoDatas.count == 1 {
        // 重做 redo  append 添加
        redoDatas.append(undoDatas.last!)
        // 撤回 undo 清空
        undoDatas.removeLast()
        // 清空图片视图
        bgView.image = nil
    } else {
        // 把 3 给 redo
        redoDatas.append(undoDatas.last!)
        // 从 undo 移除 3. 还剩 2 1
        undoDatas.removeLast()
        // 清空图片视图
        bgView.image = nil
        // 把 2 给图片视图
        bgView.image = UIImage(data: undoDatas.last!)
    }}// redo 重做@objcprivate func redo() {
    if redoDatas.count > 0 {
        // 先赋值,再移除(redo的last给图片视图)
        bgView.image = UIImage(data: redoDatas.last!)
        // redo的last 给 undo撤回数组
        undoDatas.append(redoDatas.last!)
        // 从redo重做 移除last
        redoDatas.removeLast()
    }}

关于橡皮的思路我是这么考虑的。按照现实生活中情况,使用橡皮时是把已经写在纸上的笔迹给擦除,换到项目中来看,其实橡皮也是一种画笔只不过是没有颜色的画笔罢了,并且可以有两种思路:

  • 笔迹直接加在 contentLayer 上,此时需要对橡皮做一个 mask,把橡皮笔迹的路径和底图做一个 mask,这样橡皮笔迹留下的内容就是底图的内容了;

  • 笔迹加在另外一个 layer 上。这种情况可以直接给橡皮设置成该 layer 的背景色,相当于 clearColor

第二种做法我没试过,但是第一种做法是非常 OK 的。

总结

以上就是手帐 app 的最小可行性产品了,当然还有很多细节都没有展开,比如服务端部分的代码思路。因为服务端还是围绕产品出发,设计上也不太好,是我第一次使用 Vapor 进行开发,只发挥出了 Vapor 的 10% 功力。目前服务端完成的需求有:

  • 用户的登录注册和鉴权;

  • 手帐及手帐本的创建、删除和修改;

  • 贴纸的创建、删除和修改。

如果不想与服务端进行交互,可以直接该对应按钮的点击事件为你想要展示的类,并注释掉对应的服务端代码即可。


Guess you like

Origin blog.51cto.com/14158790/2432588