How to implement a mobile account app

How to implement a mobile account app

Some time ago, I was very interested in the implementation details of the account-type apps, so I wanted to implement a minimized viable product by myself. Of course~Since it is a product under the MVP mode, it only realizes the "function", but in some places where I particularly want to "plagiarize", I also put a little effort to pursue UI performance.

Preface

When I was a kid, I was a fan of hand-written newspapers. When I was in the fourth grade, my class organized a hand-written newspaper competition. The teacher asked every student to use the weekend time to make a handwritten newspaper for evaluation. The theme was chosen. Up to now, I am still very impressed. I thought about it for a noon and didn t know what theme to choose. After I drew something on white paper, I wiped it all off, soiled several pieces of paper, and finally drew it. One earth, the mind slowly opened up.

When it was handed over to the teacher on Monday, I didn't dare to be the first one. I was at the end of the line. After receiving my handwritten newspaper, the teacher actually said: "Come on, come and see what a handwritten newspaper is." My heart rate reached a very high point at the time, and my face was red and hot. I stood beside the teacher. It's not going or not, smiling awkwardly, but with extreme pride inside.

When I arrived in junior high school, the head teacher also asked everyone to use the weekend time to make a handwritten report, because I had a little experience in elementary school, and when I arrived in junior high school, I basically used computers to assist in completing various tasks. Once it was opened, I wondered if I could make more innovations. The news of Kodak s bankruptcy came out at that time, which is equivalent to the memory of a generation~ Sometimes I would go to the old house to flip through various films, and look at the reflected image in the sunlight.

Combining this incident, I thought of using the "film" style to illustrate the theme of bird protection. I downloaded some pictures of various birds from the Internet, processed them by myself, and finally copied the report to the teacher. When it was handed over to the teacher, the teacher smiled happily, and took my handwritten newspaper to show to the students on the podium, "Look, everyone, it's pretty good~ well, it's pretty good!".

In the summer vacation after the college entrance examination, Nanguo Metropolis Daily organized a handwriting contest for elementary and middle school students. At that time, I participated in this contest as a cousin and won the third prize. The prize was a 500 yuan bookstore card for an innovative bookstore.

The above is my experience of copying newspapers or hand-painting similar to the handbook. I especially like this way of telling a story. It can be a good way to express what I want to express through some words, pictures and paintings. Way to show it.

Therefore, when the mobile account app appeared, I quickly downloaded and used it. During the use, I did achieve my original intention of telling something by organizing some elements and words. Some time ago, I had a whim, if I could make a pocketbook by myself, and by the way explore the issues that need to be paid attention to in implementing a pocketbook app, that would be great!

design

First of all, I used the top 10 searchable apps under the keyword "Mobile Account" in the App Store, and summarized some common points of Mobile Account apps:

  • Add text. Rotate, zoom in and zoom out, and rotate fonts;
  • Add photos. It can be rotated and flipped, zoomed in and out, and has simple or auxiliary image modification tools;
  • Add stickers. Using some drawn stickers, the operation is similar to "adding photos";
  • stencil. Provide a set of templates, users can add content in the area specified by this template;
  • Provide canvas with unlimited length or width.

Basically, there are so many common functions of these pocketbook apps. Because this project was done with the idea of MVP, the high-fidelity design was not achieved, and a relatively simple pocketbook app design was copied directly.

Technology stack

After you have determined the function points that you want to implement, you need to start choosing the technology stack, because after all, what you want to do is MVP products, not demos. My understanding of demos is to "implement a certain function point." The understanding of MVP products is "a complete and usable product at a certain stage." There are some problems in the details of the things that come out of the MVP model. You don t need to be too harsh, but the overall logic must be complete, and incomplete logic may not be. But once it has, it must be complete. The logic path covered may not be 100%, but the main logic must be fully covered.

Client

The technical points of iOS app development are as follows:

  • Pure native Swift development;
  • Network requests => Alamofire, some simple data directly go NSFileManagerfor file persistence manager;
  • Based on all UI components UIKitdo; social sharing go system to share, not to integrate other SDK;
  • The module provides "stickers", "brushes", "photos" and "texts". In the process of doing it, I discovered that "photos" and "texts" are essentially stickers, saving a lot of things.

Server

In fact, I have a hard requirement for every new side project I open. After finishing it, I have to increase my technical level. In fact, "growth" is very metaphysical. How to define "growth" right? I found one of the simplest ideas for myself: use new things to complete it!

So in the end I service directly without selection of the brain Vaporperformed by Swift to write server This is what I always wanted to do but could not find time to do things, to take this opportunity to get on the train before. As for why the election is not Perfect, in fact, I personally do not have to go hands-on practice too, just listen to the bigwigs say Vaporthe style of API Swiftynumber.

In the first phase of the MVP in dependence on the server is unlikely, so the current architecture is simple, can reach to get away - on Vaporthe use of some of the details, I can be in the article view, this paper will not then detailing Vaporusage details.

achieve

gesture

For mobile accounts, the core one is **"stickers"**. How to pull the sticker from the storage and put it on the canvas is solved in this step, and most of the subsequent content is also solved.

1. we need to be clear, in this project, "canvas" itself is also UIView, to add "stickers" to the canvas, in essence, is to UIImageViewgive addSubviewto UIViewthe. Secondly, the pursuit of the handbook is the control of the material. Rotating and zooming is the basic operation, and as mentioned above, we can almost consider both "photos" and "texts" as the inheritance of "stickers", so this It extracts the "sticker" itself is the base class that can provide interactive components.

The fluency of multi-gesture operations performed by mobile account apps on stickers is a factor that determines the user retention rate. Therefore, we take away the "sticker" of the handbook again, and move the basic gesture operations to a higher level of the parent class, leaving the business logic in the sticker. The core code logic of gesture operation is as follows:

//pinchGesture  
//   gesture   UI 
@objc
fileprivate 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 
@objc
fileprivate func rotateImage(gesture: UIRotationGestureRecognizer) {
    if gesture.state == .changed {
        transform = transform.rotated(by: gesture.rotation)
        //0 
        gesture.rotation = 0
    }
}

//panGesture /
//   gesture   UI 
@objc
fileprivate 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 
@objc
fileprivate func doubleTapGesture(tap: UITapGestureRecognizer) {
    //   
    if tap.state == .ended {
        //  90 
        let ratation = CGFloat(Double.pi/2.0)
        //      =   +  
        transform = CGAffineTransform(rotationAngle: previousRotation + ratation)
        previousRotation += ratation
    }
}
 

The effect achieved is shown in the figure below:

Use UICollectionViewas stickers container closure by the click event icon image corresponding to the index mapping sticker object to instantiate a parent view:

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)
}
 

In the parent view, the sticker object is received by implementing a closure, thus completing the whole process from **"sticker" to "canvas"**.

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)
}
 

"Photo" and "Text"

Did not do a good job design, Logically, should be directly on a PDA before the bottom of the toolbar to edit the page UITabBarto get away, but eventually also used UICollectionViewto complete. Photo reading device operation is relatively simple, does not require custom albums, so through the system UIImagePickercomplete, custom albums for students interested can read my article . The code details of the top toolbar are as follows:

// 
collectionView.cellSelected = { cellIndex in
switch 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
}
 

Each module is a toolbar at the bottom of UIViewthis section do not very good, the best approach should be based on UIWindowor UIViewControllerdo a "tool container" as the container contents of each module UI element, this approach can avoid Go write so many view display/hide status codes in the click event callback of the bottom toolbar.

Watch block "photo" section, implement UIImagePickerControllerDelegatethe method after the agreement:

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)
        }
    }
}
 

Text

The text module exposed to the parent view is also an instantiated sticker object, but the text color, font, and font size need to be selected in the text VC. After finishing it, I discovered that because the sticker can be zoomed in and out by gestures, there is no need to choose the font size...

One of the more strenuous is the choice of text color. At first I thought about RGB color toning directly, but later I thought that if there are three channels directly through RGB, it would be very uncomfortable to adjust the color. I thought of the HSB color mode used in the game "Crazy Pinball" and made a disc color selector. Later, in the process of thinking about the implementation details, the library EFColorPicker written by EF was very easy to use. I changed it. Use it directly after the UI, thank you EF!

The "bubble view" itself is one UIViewController, but several properties need to be set. The implementation process is more streamlined, and a better way is to encapsulate it and turn these templated codes into a "bubble view" class for business parties to use, but because of the time relationship, they have been copying. The core code is as follows:

/// 
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
    }
}
 

In view of the need to pop the bubbles through the place presentyou can call:

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
    }
}
 

brush

Before the internship at the bit, wrote about a component of the Brush (actually already two years ago ...), but the brush is based on the drawRect:method to do it, for memory is very unfriendly, has been drawn down, memory remains up, this time using a CAShapeLayerrewrite one, the results were good.

Based on the brush before withdraw drawRect:way to do it would be very simple, once the withdrawal is equivalent to redraw every time, the line was withdrawn from the group plotted points removeout just fine, but based on the CAShapeLayerrealization not the same, because each of a sum is generated directly in the layermiddle, and if you have to withdraw the current need to regenerate layer.

So in the end, my approach is to generate a picture for each stroke and save it in the array. When the recall operation is performed, the last element in the recall array is replaced by the content of the current drawing canvas and moved from the recall array. Except this element.

With the withdrawal, it must be reworked. The redo is to prevent the withdrawal, which is similar to the withdrawal. Then create a redo array, each removed from the withdrawal of the array are out of the picture appendarray to redo. The following is the core code of the withdrawal and redo:

//undo  
@objc
private 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  
@objc
private 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()
    }
}
 

This is how I think about the idea of rubber. According to the situation in real life, when using the eraser, the handwriting that has been written on the paper is erased. When looking at the project, the eraser is also a kind of brush, but it is a brush with no color , and there are two ways of thinking. :

  • Handwriting directly added contentLayeron at this time need to make a rubber mask, the rubber handwriting path and do a base map mask, so leave rubber handwriting content is the content of the base map;
  • In a further plus handwriting layeron. This situation can be directly set to blanket the layerbackground color, corresponds clearColor.

I haven't tried the second approach, but the first approach is very OK.

summary

The above is the minimum viable product of the mobile account app. Of course, there are still many details that have not been expanded, such as the code idea of the server. Because the service is starting around the end product, the design is not very good, is my first time Vaporto develop, play only a Vapor10 percent skill. The current requirements completed by the server are:

  • User login registration and authentication;
  • Creation, deletion and modification of handbook and handbook;
  • Creation, deletion and modification of stickers.

If you don't want to interact with the server, you can directly use the click event of the corresponding button as the class you want to display, and comment out the corresponding server code.

project address:

Reference link