Auto Layout and UIScrollView seem to be a popular, but sometimes problematic couple. So here is another post about this topic. This time we will take a couple of subviews, add them to a UIScrollView and make everything work by programmatically adding layout constraints.
Because I am a lazy guy, we will use SnapKit (a.k.a. TheFrameworkFormerlyKnownAsMasonry) for the Auto Layout stuff. And because I get bored easily and I have already written a post about a vertical UIScrollView with Auto Layout, we’ll do a horizontal UIScrollView this time. Yay!
So, let’s take 4 empty UIViews and give them each a different background color, so that we can see the scrolling. To make things simple we make each subview the same size as the scrollview (so each subview covers the whole visible area).
Here is the code, you’ll find some explanations below it:
import UIKit
import SnapKit
class ViewController: UIViewController {
let scrollView = UIScrollView()
let subViews = [UIView(), UIView(), UIView(), UIView()]
let colors = [UIColor.greenColor(), UIColor.blueColor(), UIColor.redColor(), UIColor.orangeColor()]
override func viewDidLoad() {
super.viewDidLoad()
// 1
view.addSubview(scrollView)
scrollView.snp_makeConstraints { (make) in
make.edges.equalTo(view)
}
// 2
subViews.enumerate().forEach { index, subview in
subview.backgroundColor = colors[index]
// 3
scrollView.addSubview(subview)
subview.snp_makeConstraints(closure: { (make) in
// 4
make.top.equalTo(0)
make.size.equalTo(scrollView)
switch index {
// 5
case 0:
make.left.equalTo(0)
// 6
case subViews.count - 1:
make.left.equalTo(subViews[index - 1].snp_right)
make.right.equalTo(0)
// 7
default:
make.left.equalTo(subViews[index - 1].snp_right)
}
})
}
}
}
First we add the UIScrollView to the UIViewController and set its size to the size of the UIViewController’s view. // 1
We iterate over the subviews // 2, add them to the UIScrollView // 3 and add the Auto Layout constraints following this 4 simple rules:
1. All subviews have a top constraint of 0 and a size constraint that is equal to the scroll view. // 4
2. The first subview (in other words the leftmost subview) has a left constraint of 0 // 5
3. The last subview (the rightmost subview) has a right constraint of 0 // 5
4. All other subviews have a left constraint to the right of the previous subview // 6
And that’s all. When you run this code you can rotate the device to see that the subview sizes and the UIScrollView’s contentSize are adjusted automatically. If you don’t want the UIViews to be resized you just have to give them a static size.
This example also works for a vertical UIScrollView. Just connect the top and bottom constraints of the subviews instead of the left and right constraints. See the blog post I mentioned above for more details.
In a previous post I described how to programmatically set the height of a UIWebView to fit the height of its HTML content.
This is a different approach using a Storyboard (and a little code). To make things a little bit more interesting I added an UIView that sits on top of the UIWebView and that should scroll out of view when the user scrolls the UIWebView. To make that happen we need an UIScrollView that contains the UIView and the UIWebView:
The UIView could be something like an iAd that you want to display on top of the web content but that should be scrolled out of the view when the user scrolls the web content.
Here is how to make it work:
Connect the UIWebView from the nib to an outlet in your view controller.
Disable scrolling in the UIWebView.
Set the constraints on the `UIScrollView`, the `UIView` and the `UIWebView`:
The UIScrollView needs a top, a bottom, a leading and a trailing constraint to the UIViewController’s view.
The UIView needs a top, a leading and a trailing constraint to the UIScrollView. It also needs a width constraint that is equal to the UIScrollView’s width to avoid horizontal scrolling (See this post for an explaination). I also add a height constraint, because I want to have the UIView to have a constant height of 100pt.
The UIWebView needs a top constraint to the UIView’s bottom, a leading, a trailing and a bottom constraint to the UIScrollview. It also needs a height constraint that we will later set to the height of the HTML content
The constraints should look like this:
Connect the UIWebView‘s height constraint to an outlet in your view controller.
Set the view controller as UIWebViewDelegate.
In webViewDidFinishLoad set the height constraint’s constant to the height of the contentSize of the scroll view inside the web view.
Start Key-Value Observing on the contentSize to change the height, when height of the web view has to change because segments of the webpage change their size without reloading the page (like accordeons, or menus). Don’t forget to stop observing when the view controller gets deallocated.
When you have a UITableView with a lot of sections a section index can be very useful to jump quickly between sections. A good example for this is you iPhone’s address book.
But sometimes you want to hide the section index until the user scrolls the table view. The problem is that you can neither hide or show the section index directly nor officially access the UIView that holds the section index.
However there is a little trick to fade the section index in and out. UITableView has two methods that allow you to set the background and the text color of the section index. When you change the alpha of those two colors according to the UITableView’s contentOffset you can fade the section index when the user scrolls:
Some people still seem to struggle when it comes to using Apple’s Auto Layout in a UIScrollView. There are a lot of questions on StackOverflow like “Why is my UIScrollView not scrolling when using AutoLayout?”
So here is a short explanation on how to use Auto Layout with a UIScrollView that should scroll vertically:
There are just a few things you have to take care of:
1. The topmost subview must have a top constraint with the UIScrollView
2. All other subviews must have a top constraint with the bottom constraint of the subview above them
3. The bottommost subview must have a bottom constraint with the UIScrollView
To ensure that the UIScrollView only scrolls vertically you have to make sure that its subviews don’t become wider than the UIScrollView.
Do not rely on left and right constraints to define the width of a subview. If for example you have a UILabel that has a lot of text and should break into several lines, it just won’t, even if you set its numberOfLines property to 0. That’s because the UIScrollView will give it enough space by allowing horizontal scrolling. So if you just set a left and right constraint on the UILabel the UIScrollView will scroll horizontal and the label will be very wide and have only 1 line.
Instead you should define a left and a width constraint. Set the width constraint to the width of the UIScrollView and the UILabel will not become wider than the UIScrollView. It will wrap into multiple lines instead.
If you follow those steps you don’t have to set the UIScrollView’s contentSize property any more to make the UIScrollView scroll. Auto Layout will handle that for you.
To make it more clear, here is an image with the constraints that you have to set:
If you are using Masonry or SnapKit here is a code example on how to set the constraints programmatically:
With the introduction of throwable in Swift 2 more and more functions implement the new error handling. However there are still some functions that expect you to pass an error pointer. This is especially the case when you are using an Objective-C framework in your Swift project.
So for an example let’s look at AFNetworking’s AFHTTPRequestSerializer’s method requestWithMethod:URLString:parameters:error:
If you are trying to implement it like you would in Objective-C you are in for a surprise:
var error: NSError
let request = AFHTTPRequestSerializer.serializer().requestWithMethod("GET", URLString: "https://yourDomain.com", parameters: nil, error: &error)
This will cause the following compiler error:
'&' used with non-inout argument of type 'NSErrorPointer' (aka 'AutoreleasingUnsafeMutablePointer>')
The fix for this is much more simple than the error message suggests. The thing is, that error can be nil if the method is successful. Because of that, error has to be an Optional:
var error: NSError?
let request = AFHTTPRequestSerializer.serializer().requestWithMethod("GET", URLString: "https://yourDomain.com", parameters: nil, error: &error)
One of the new operators that comes with Swift is the Nil Coalescing Operator. In the documentation it is explained as: The nil coalescing operator (a ?? b) unwraps an optional a if it contains a value, or returns a default value b if a is nil.
This operator already exited in C but for Objective-C developers it is kind of new (more about that at the bottom of this post).
With this operator assigning values to a property with a fallback to a default value becomes really concise:
Instead of doing this:
func setupBackgroundColor(color: UIColor?) {
if (color != nil) {
backgroundColor = color
} else {
backgroundColor = UIColor.whiteColor()
}
}
You can simply do this:
func setupBackgroundColor(color: UIColor?) {
backgroundColor = color ?? UIColor.whiteColor()
}
Pretty cool, isn’t it?
Actually something like this was already possible with Objective C:
Creating a Share Extension in iOS8 is really easy:
In your app choose File > New > Target
Choose Application Extension > Share Extension
Enter a name for your Share Extension and press Finish
That’s all! Xcode creates everything you need for you and puts it in it’s own folder. You’ll find a ShareViewController that is a subclass of SLComposeServiceViewController. That class’ UI looks a lot like the SLComposeViewController that you’ve probably worked with when using iOS’s Social Framework (to share your app’s content to Twitter or Facebook).
In ShareViewController Xcode creates some stubs for you where you have add the code to actually share the content to your service. In my case I want to build an extension for Safari that allows the user to share the page’s URL on my service. So when the user selects my share extension from Safari I need to retrieve the page’s URL to send it to my server.
When the user starts my Share Extension and presses the “Post” button the method didSelectPost is called on the ShareViewController.
Let’s have a look at the stub method that Xcode created:
- (void)didSelectPost {
// This is called after the user selects Post.
// Do the upload of contentText and/or NSExtensionContext attachments.
// Inform the host that we're done, so it un-blocks its UI.
// Note: Alternatively you could call super's -didSelectPost,
// which will similarly complete the extension context.
[self.extensionContext completeRequestReturningItems:@[]
completionHandler:nil];
}
So far so good. So I added the code to retrieve the page URL. Compared to the easiness of creating the whole extension, the simple task of retrieving the URL takes quite some effort:
- (void)didSelectPost {
NSExtensionItem *item = self.extensionContext.inputItems.firstObject;
NSItemProvider *itemProvider = item.attachments.firstObject;
if ([itemProvider hasItemConformingToTypeIdentifier:@"public.url"]) {
[itemProvider loadItemForTypeIdentifier:@"public.url"
options:nil
completionHandler:^(NSURL *url, NSError *error) {
NSString *urlString = url.absoluteString;
// send url to server to share the link
// BUT THIS BLOCK IS NEVER EXECUTED!!!
}];
}
[self.extensionContext completeRequestReturningItems:@[]
completionHandler:nil];
}
That’s a lot of code for just an URL. And the worst thing: It’s not working! The completion handler of the loadItemForTypeIdentifier:options:completionHandler: is never called.
As it turns out calling completeRequestReturningItems:completionHandler: at the bottom of the method causes this problem. In Apple’s documentation it says that you are supposed to call this method to ‘Tell the host app to complete the app extension request with an array of result items’. Calling this methods dismisses the ShareViewController and deallocates it. So the NSItemProvider that contains the URL is also destroyed before it can access the URL.
So the solution is quite simple: The call to completeRequestReturningItems:completionHandler: has to be done AFTER retrieving the URL and sending it to the server. So it has to go into the completionHandler block:
- (void)didSelectPost {
NSExtensionItem *item = self.extensionContext.inputItems.firstObject;
NSItemProvider *itemProvider = item.attachments.firstObject;
if ([itemProvider hasItemConformingToTypeIdentifier:@"public.url"]) {
[itemProvider loadItemForTypeIdentifier:@"public.url"
options:nil
completionHandler:^(NSURL *url, NSError *error) {
NSString *urlString = url.absoluteString;
// send url to server to share the link
[self.extensionContext completeRequestReturningItems:@[]
completionHandler:nil];
}];
}
}
And now the URL is retrieved and can be posted to my server.
In case you need to do this in Swift, here is how that would look like:
override func didSelectPost() {
if let item = extensionContext?.inputItems.first as? NSExtensionItem {
if let itemProvider = item.attachments?.first as? NSItemProvider {
if itemProvider.hasItemConformingToTypeIdentifier("public.url") {
itemProvider.loadItemForTypeIdentifier("public.url", options: nil, completionHandler: { (url, error) -> Void in
if let shareURL = url as? NSURL {
// send url to server to share the link
}
self.extensionContext?.completeRequestReturningItems([], completionHandler:nil)
})
}
}
}
}