preface

A new side project has recently been launched to host WWDC19 announcements using the following technology stack:

  1. SwiftUIDo all the presentation layers.
  2. Alamofire + SwiftyJSONDo all the network layer interactions, originally wanted to do another oneMoyaOn second thought, the product network layer is relatively simple, there is no need to go up.
  3. SPM manages all third party dependencies. As far as the current usage is concerned, it is better than POD experience and will continue to be used.
  4. MVC is still used, but this time MVC is just a simple “module partition”, and Workflow and Dataflow follow along as much as possibleSwiftUIThe official recommended practice to.
  5. useCore Data + FileManagerManage all data caches.
  6. useSF SymbolsDo all the ICONS.
  7. Because another focus is on dark mode, we use Group to set light mode and Dark mode for direct preview.

After doing some early work, I’ve recently been working on implementing “more menus” in my spare time. “More Menus” on Github search keywords contextMenu/menu, and then limit the language, will come out a bunch of UIKit based implementation, if we want to implement a SwiftUI style “more menus” how to achieve?

UIKitWhat do you do?

Before implementing “more menus” with SwiftUI, let’s take a look at how to do it with UIKit. Because UIKit is relatively familiar to us, and most apis are familiar to us, we won’t go into the implementation details.

  1. There are two ways to do this. If it’s full screen “more menus”, it might do a “more menus” ViewController transition based on CATransition, or it might do a keyWindow transition based on UIWindow, so you’re making a container.

  2. Now that we have a container, we can create a simulated “list” view based on a UITableView or For loop, and then we can use the closure to receive the data source configuration from the ViewModel, and use the closure to pass the click event from the “More menu”.

  3. Once you’ve created the “more menus” inside the container, tweak the layout constraints, get the screen width and height and so on, then encapsulate a nice API, expose it to the business caller, and leave it to QA for feedback to tweak. The wrapped invocation might look something like this (this is a selector component I’ve customized

    PJPickerView.showPickerView(viewModel: {
        $0.dataArray = [["PJHubs"."PJ"."Elastic"], ["Pei jun".Jun "o"]]
        $0.titleString = "Choose your nickname."{})print("The nickname chosen is:\ [$0)")
        print("The selected index is:\ [$1.section)\ [$1.row)")}Copy the code

But can this UIKit idea run directly on SwiftUI?

SwiftUIWhat should be done?

When talking about how SwiftUI should achieve “more menus”, let’s assume that we are all familiar with the basic grammar of SwiftUI and Tutorials on its official “SwiftUI”.

If you’re a UIKit player like me, then we’re also thinking:

  1. Create a container.
  2. Create a list view in the container.
  3. Use a state variable to control the display and hiding of “more menus”.
  4. Exposes a closure that tells the more Menu dependent superview which option was clicked.

It all seems to be on the right track. In my practice, it is true that this idea is correct. It can be seen that Apple has not abandoned the habit of thinking developed in UIKit. However, when preparing to do it, it found some strange places……

How do I create a container?

When we switch from UIKit to SwiftUI, we’ll see that a View is no longer a UIView. You can’t even create an Array

set of views, but in SwiftUI everything is a View (except for the basic main views like Text, Image, Color, etc.

We cheerfully used VStack and HStack, possibly with a ForEach, to create a list of “more menus” based on incoming data sources:

There was a problem when we tried to put the “more menus” prototype on the navigation bar of the home list, and when we added the menu prototype directly to the written list, it was covered in full screen!

On reflection, a View in SwiftUI is not a UIView, which is very important to keep in mind! When we use a state variable to control menu display and hide, we add a View, and when it is hidden, SwiftUI only renders the original list; When it does, it triggers SwiftUI’s diff algorithm to re-render what should be rendered.

So even if you re-render, why do you “lose” the original rendered list? I looked around and found no answers. The following is a guess:

First we need to make it clear that SwiftUI is a “declarative” layout. When we want to return a whole View to the body, we return a “bunch” of views, i.e. menus and lists. At this point, the menu View and list View are not a collection, i.e. we return two views, but if we stretch out the code, in Swift 5.1 when there is only one value to return, the return can be omitted.

However, the content set inside NavigationView is missing the layout, so that the list performs DSL, but when converted to draw information, the data of the draw list is lost. This explains why we stopped at SwiftUI breakpoint but did not see the corresponding View Hierarchy in Xcode’s Debug View Hierarchy.

Once you know what the problem is, add a VStack to solve it.

But what we actually find is that menus and lists are mixed in the same hierarchy, and if you think back to UIKit menus, as we said, we would use UIViewController or UIWindow to separate menus from their parent view on the vertical axis, and the same thing with SwiftUI, So we need to use ZStack.

We can find that ZStack still doesn’t work, so let’s go back to UIKit and think, when we use UIKit to complete the menu, will we switch the view level? How do I switch view levels in SwiftUI?

Unfortunately, there is no way to switch view hierarchy in SwiftUI, there is only a state variable value to control the display and hiding of a view, but SwiftUI is just a DSL and will eventually be translated to render node tree, so we can assume that menus are definitely blocked by lists.

So just add the menu under the list.

Adjust list constraints

We need to move the list to the top left and add arrows. At this point, we have the prototype implemented, and we need to wrap the library into a MenuView for external calls.

If we set the frame to the VStack directly, it will not work because the VStack has no geometry boundary, then we should package the layer menu view with the GeometryReader and set the frame for the GeometryReader.

In other words, the container of the menu is now changed from VStack to GeometryReader. When we go to Debug View Hierarchy, we can see that both the menu and the list are in the same View. We just need to make the container transparent. Then add click events to the GeometryReader to control the display and hiding of the menu.

Note that in SwiftUI, if you give a View a clear background color, the View will not be rendered, so opacity is 0.01.

As a matter of fact, if I build the project up to now, the effect is similar from the UI. If it is a plain text menu, the content of this link is basically over. However, I still want to use SF Symbols, so I made a menu of “words on the left and right”.

To my surprise, SF Symbols is not a neat square icon. When you throw it on the menu without any processing, you will find that each line of Symbols and words have some offsets. If you call methods like.resizable(),.frame, and.scaledtofill () directly, you’ll notice that the icon is distorted again.

Again, “Any problem in the field of computer science can be solved by adding an indirect intermediate layer.” The above problems will occur when Image and Text are nested in the same HStack, so it is good to set another HStack for Image to restrict the Image.

This is the menu cell component I’ve wrapped.

struct MASSquareMenuCell<Content: View> :View {
    var itemName: String
    var itemImageName: String
    var content: () -> Content
    
    var body: some View {
        NavigationLink(destination: content()) {
            HStack {
                / / limit ` Image `
                HStack {
                    Image(systemName: itemImageName)
                        .imageScale(.medium)
                        .foregroundColor(.white)
                        .padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 20))
                }
                    .frame(width: 50)

                Text(itemName)

                Spacer()
            }
                .foregroundColor(.white)
                .padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5))
                .frame(width: 130)}}}Copy the code

Data source Settings

With the layout constraints set, it’s just time to plug the data. Because the development process used by SwiftUI is quite different from my previous development process, especially in the area of data flow. After reading several projects on Github, I realized what was going on.

I discussed with Mentor about whether this kind of menu component should be made into a UI component or a business component. Finally, I came to the conclusion that it depends on the specific needs of the business. If it is a simple UI component, every click of the menu item should be exposed to the owner who manages its life cycle. If the component does something more closed, leaving little to the business caller to customize, and is really accessible with a single line of code or a simple configuration, it is fine to make it pure business.

First of all, I have implemented similar “more menus” in other Side projects before, but at that time, due to the influence of UIKit and the code style of the internship company, I developed a UI component, no matter what the external presentation is, it will be all UI components. But the development model SwiftUI espouses got me thinking.

Finally, after some sorting and following the principle of “premature optimization is the devil”, combining business and UI component patterns, I determined that every option click on the menu should jump through NavigationLink, and then I needed to expose a closure for the caller to fill in the view of each option in the menu.

At the beginning, my idea was very simple. I still wanted to create a menu data source middleware according to UIkit’s idea, and the caller could dynamically add and delete options in the menu. There was nothing wrong with this mode, but SwiftUI did not support it.

ItemName and itemImageName in the menu are easy to think about, but itemView inherits from View? Obviously not, because View is a protocol, but what if I inherit View and implement a class or a structure? Remember, to implement the View protocol you need to declare the body property as well, but the purpose of the dynamic add/remove option is to dynamically change the content of the View

Going back to the UIkit idea, if we wanted to implement a menu data source model, we might write something like this:

struct MenuModel {
    var itemName: String
    var itemImageName: String
    var itemView: UIView
}
Copy the code

We’ve explicitly specified that itemView is of type UIView. In SwiftUI, the same is true for achieving this effect. Since we can’t avoid declaring the body property of the View, we should implement it.

//
// MASSquareMenuView.swift
// masq
//
// Created by Wengpejun on 2019/8/2
// Copyright © 2019 PJHubs. All rights reserved
//

import SwiftUI

struct MASSquareMenuCell<Content: View> :View {
    var itemName: String
    var itemImageName: String
    var content: () -> Content
    
    var body: some View {
        NavigationLink(destination: content()) {
            HStack {
                / / limit ` Image `
                HStack {
                    Image(systemName: itemImageName)
                        .imageScale(.medium)
                        .foregroundColor(.white)
                        .padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 20))
                }
                    .frame(width: 50)

                Text(itemName)

                Spacer()
            }
                .foregroundColor(.white)
                .padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5))
                .frame(width: 130)}}}struct MASSquareMenuView<Content: View> :View {@Binding var isShowMenu: Bool
    var content: () -> Content
    
    
    var body: some View {
        GeometryReader { _ in
            // Top arrow
            Image(systemName: "triangle.fill")
                .padding(EdgeInsets(top: 5, leading: 25, bottom: 0, trailing: 0))
            
            VStack(alignment: .leading) {
                self.content()
            }
                .background(Color.black)
                .cornerRadius(5)
                .padding(EdgeInsets(top: 10, leading: 10, bottom: 0, trailing: 0))
            
            Spacer()
        }
            .background(Color.white.opacity(0.01))
            .frame(minWidth: UIScreen.main.bounds.width, minHeight: UIScreen.main.bounds.height)
            .onTapGesture {
                self.isShowMenu.toggle()
            }
    }
}
Copy the code

VStack (@viewBuilder, @functionBuilder); VStack (@viewBuilder, @functionBuilder); See this article by Meow God for more details.

We just need to follow the following way to call, you can elegantly complete the menu data source filling.

Afterword.

Again, this is my side project for hosting the various new frameworks of WWDC19, and my understanding of many things is constantly developing. From Beta1 to Beta5 I looked at almost 60% of all SwiftUI related repos open on Github, everyone was working on Apple’s official demo, and there were some real issues like “more menus” that no one was addressing, Most are doing various variations of the todo-list.

This project has not yet been completed or even just started. In terms of the realization of some ideas, SwiftUI is so new that I have no reference to learn about or similar needs. I can only say that I have withstood a lot of self-imposed pressure.

The resources

Some preliminary explorations of SwiftUI (1)

Some preliminary explorations of SwiftUI (2)

Custom view won’t use state variable update provided through binding, but debug watch shows changes

hite/YanxuanHD

Fucking SwiftUI

SwiftUI data flow

How do I create a multiline TextField in SwiftUI?

Behind SwiftUI

SWIFTUI BY EXAMPLE

Project address: Masq iOS client

PJ development routine: PJ development routine