LifeAnalysis Lab & Yuxiao

Swift 3 - Add SafariViewController as childViewController with SnapKit

Intro

This article is trying to add SafariViewController as childviewcontroller. I’m using SnapKit to auto layout everything and Swift 3 as the language.

I also introduced some divide and multiply using of SnapKit that not describing much in official documents.

Download final Project here.

中文版教程:Swift 3 - 用 SnapKit 添加 SafariViewController 为 childViewController

Dev Person:Developer the World.

SnapKit

SnapKit is a DSL to make Auto Layout easy on both iOS and OS X.

Why using?

When I develop Info It, I encountered a problem that when I trying to add constraints at Xcode interface, the Xcode give me crash every time I using it. And after I installed SnapKit inside, I found it was something wrong with Xcode that I cannot find out why (searching Google, Stackoverflow, etc won’t give me any answer). After I installed SnapKit and using it, it solved my problem automatically and the elegant code of SnapKit makes constraints, auto layout beautiful and easy.

Install

SnapKit Document

Everyone can install it quickly using CocoaPods. This article will not describing details about install CocoaPods, I will directly install SnapKit, assuming you all installed CocoaPods.

Create a new project with Swift language. Open Terminal while Xcode project still open(I using iTerm). Then ‘cd’ to your project address, and init the podfile

> pod init

After this command, add this line inside your podfile, the place like I did in picture.

pod 'SnapKit', '~> 3.0'

Then install pod using commend:

> pod install

While install SnapKit, do not close the project! After install successfully like picture, close the project and open the .xcworkspace file


SafariViewController

I will using “svc” for convenient for SafariViewController.

While you want using SafariViewController, you need to add SafariViewControllerDelegate after your class, and set two String we will using later as links:

1
2
3
4
5
class ViewController: UIViewController, SFSafariViewControllerDelegate {

// Insert two web link to test the child view
let main: String = "http://o1xhack.com/2017/02/21/infoit/"
let update: String = "http://o1xhack.com"

Create a view and a button in the storyboard. (button using to test how to auto layout and also the change link of svc)

And connect the view and button into the controller file.

addChildViewController

Then add the code inside viewDidLoad function about the auto layout and also childviewcontroller.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
override func viewDidLoad() {
super.viewDidLoad()

showView.snp.makeConstraints{ (make) -> Void in
make.edges.equalTo(self.view).inset(UIEdgeInsetsMake(0,0,60,0))
}

let svc = SFSafariViewController(url: URL(string: main)!)
svc.delegate = self
self.addChildViewController(svc)
self.showView.addSubview(svc.view)
svc.didMove(toParentViewController: self)

svc.view.snp.makeConstraints{ (make) -> Void in
make.edges.equalTo(self.showView)
}
}

line 4-6:

Make constraint of the showView(the view will be the parent of our svc), this code using SnapKit to make constraint of the showView compare to view, with bottom 60 to the view. Which will make an empty space of the bottom only.

line 8-12:

Line 8 set svc to a new SafariViewController with url of my previous settings. And line 9 set the svc delegate to self.
Line 10 add the svc as childviewcontroller, and then we can using it add to showview’s subview in line 11.
Line 12 tells the controller that the svc has moved to parentviewcontroller, which is the showView.

line 14-16:

This is basic the same as line 4-6, we set svc’s contraint just fit the showView, kind like set it to view(0,0,60,0), but since svc is the child view, we only need to set it’s constraint to it’s parent view.

Result

After these codes, we run the project and will get this kind of simulation that the button was covered by the svc, which caused by we not setting any constraint with it.


SnapKit again

Once for all, we know how to constraint a view with spacing 60 to the bottom, let’s do some work to set constraint of the button based on the showView and view.

1
2
3
4
change.snp.makeConstraints { (make) -> Void in
make.centerX.equalTo(showView)
make.top.equalTo(showView.snp.bottom).offset(20)
}

Here for the code of change.snp.makeConstraints, we will need to add this part directly after the showView.snp.makeConstraints since we need to make constraint after and based on the showView.

For line 2 we make the button center horizontally with showView, if you using Xcode interface to drag that before, it will be familiar to you.
For line 3, we set the top of button has an offset distance of 20 to bottom of the viewShow. Since we make constraint that the viewShow will always be 60 to the bottom to the view, we can be sure the 20 distance will always working no matter the devices are. (Later, I will try to using another way of divide to set the layout, which is more elegant way.)

Run project, and you will see the button is just under the svc and center exactly same with svc.


Update SVC

Now we need to write a new function to update the svc link once we click our button, and this is not quiet difficult, we just need to drag the button to our code and create an action.

When we trying to add code in this function same as the one we add in viewDidLoad, and run the project, we will find that after we click the button, nothing happened!

The code we add are right here: So why is this happen? (Remember you cannot just copy this code sicne the IBAction need you to connect yourself.)

1
2
3
4
5
6
7
8
@IBAction func changeWebsite(_ sender: UIButton) {
let svc = SFSafariViewController(url: URL(string: update)!)
svc.delegate = self

self.addChildViewController(svc)
self.showView.addSubview(svc.view)
svc.didMove(toParentViewController: self)
}

Well, that is because we don’t add any constraint to the svc we create in this function, so the system don’t know the layout the svc and where it should be. So we trying to add svc.snp.makeConstraints same as the one in viewDidLoad and the code become:

1
2
3
4
5
6
7
8
9
10
11
12
@IBAction func changeWebsite(_ sender: UIButton) {
let svc = SFSafariViewController(url: URL(string: update)!)
svc.delegate = self

self.addChildViewController(svc)
self.showView.addSubview(svc.view)
svc.didMove(toParentViewController: self)

svc.view.snp.makeConstraints{ (make) -> Void in
make.edges.equalTo(self.showView)
}
}

Then we run the project and click the button, we will find the svc change the link and load a new website: (the first load is the article, and second is the main page.)


SnapKit Divide / Multiply

This project is basically done with all, now you know how to using SnapKit to auto layout then to make safari as an childview in anywhere. Well, but using precise number of offset is not very good. Why? Cause in the picture, you will find we don’t consider that height of the button and it has more white space from bottom of showView to top of button than bottom of button to bottom of view.

So we will using a more auto way: Divide the total view height and using fraction to auto layout it.

Now consider that we only need relatively little space for button and most of our view to svc, I will assume we divide the view into 10 parts, and the svc occupied 9 out of 10. So we are trying to divide the view into parts, but when in code we actually need to multiply the height with 0.9 because dividing height into 9/10 is not quick than multiply (Using your math skill to calculate).

1
2
3
4
5
6
showView.snp.makeConstraints{ (make) -> Void in
make.top.equalTo(self.view).offset(0)
make.right.equalTo(self.view).offset(0)
make.left.equalTo(self.view).offset(0)
make.height.equalTo(self.view).multipliedBy(0.9)
}

We change the make edge into details of top, right, left since we need a limit of height rather than based on edges. Line 2-4 makes the showView has no offset to the view of top, right and left. Line 5 set the height to be the 90% of the height of the view(superview). Run the project and you will find this really works:

Then we will handle the button layout that also using multiply and divide way. To make the button fit equally with consideration of its height, we will divide the rest part of view(1-0.9=0.1) into 3 parts. Equally fraction of top white space, height and bottom white space. this means we multiply the height with 0.1*0.3=0.03 and same as the white space from bottom of showView and top of button. Let’s try it:

1
2
3
4
5
change.snp.makeConstraints { (make) -> Void in
make.centerX.equalTo(showView)
make.top.equalTo(showView.snp.bottom).multipliedBy(0.03)
make.height.equalTo(self.view).multipliedBy(0.03)
}

Woops! We got really wired one:

Why is this happening?? That is because we are thinking a way that computer doesn’t understand!

So actually we want the rest 10% of view divide into 3 parts, that is 0.1/3=0.0333… which is infinite, so we just set the height to 0.034 of the height and the other two white space to 0.033. But what we need to do is tell the computer that it is 0.967(1-0.967) to the bottom compare to the view, not 0.03 compare to the showView. This is how multiply and divide works, we actually multiply the object’s height, not what we think that auto calculate the distance of white space, so changing the code into the following:

1
2
3
4
5
change.snp.makeConstraints { (make) -> Void in
make.centerX.equalTo(showView)
make.bottom.equalTo(self.view).multipliedBy(0.967)
make.height.equalTo(self.view).multipliedBy(0.034)
}

And it worked!

Why using?

You can see that multiply and divide can be quite confused especially you have two or more objects need to layout, but once for all, when you done the calculation, you will be good from that day to the day you need to add a new object! You will not need to consider different devices and the impact of using number as distanced to caused some button or label be covered by others.