PIXEL
DOCK

I like the smell of Swift in the morning…

Programmatically add a dynamic number of subviews to a UIScrollView using Auto Layout

Posted: | Author: | Filed under: iOS, Swift | Tags: , , | No Comments »

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.

How to make your Image Share Extension work in Safari

Posted: | Author: | Filed under: iOS | No Comments »

So you have developed this nice Share Extension to share images. Everything works fine when you select “share” in the Photos App: Your extension is visible and you can use it to share photos from your phone’s photo library.

But then you try to share images from a webpage in Mobile Safari. You press the Share Icon and … your Share Extension does not show up 🙁

What went wrong?

The solution to this problem lies in your Share Extension’s plist file. There you define what types of files your Share Extension can share. Because in your case the Share Extension only accepts image files this should be fine:

NSExtensionActivationRule

    NSExtensionActivationSupportsImageWithMaxCount
    5

This works for the Photos App. But it does not work in Safari. To enable your Share Extension in Safari you have to add one more activation rule:

NSExtensionActivationRule

    NSExtensionActivationSupportsImageWithMaxCount
    5
    NSExtensionActivationSupportsWebURLWithMaxCount
    1

Now you can share images in Safari via your Share Extension 🙂

Setting a custom GPS location in the iOS Simulator and simulate movement

Posted: | Author: | Filed under: iOS, Xcode | Tags: , , | 6 Comments »

Xcode has a neat built in feature that let’s you simulate a (GPS) location in the iOS Simulator. To switch it on you just need to run your app in the Simulator and then click on the location arrow on top of the debug area. Xcode offers a handful of default locations from Moscow to Honolulu that you can use if you just need any location.

Cool!

But sometimes you need to simulate a certain location that is not part of that default set. For example, let’s say you are developing an app that shows all the cool bars in your hometown. Then being able to simulate the location “Honolulu” won’t be of much use to you (unless your hometown in Honolulu).

Luckily Xcode offers the possibility to add a GPX file to your project that defines the location you would like to simulate. GPX is a XML Format for GPS data. It has a large set of datatypes but to simulate a location we only need the wpt (waypoint) datatype:

To add your custom location to the Simulator you have to add a GPX file to your project. Xcode offers this file type in its New File dialog (File -> New -> File… -> Resources -> GPX File) but it is just a plain XML file so you can create it somewhere else and just add it to your project (just make sure it has a “.gpx” file ending).

So to simulate the location add the following GPX file to your project:



    

Now if you run your app in the Simulator and click on the location arrow on top of the debug area, you will find your custom location on top of the default locations (Xcode displays the file name of your GPX file as the name of your custom location). Select it and the Simulator pretends to be at that location.

Very cool!

But it gets better! What if a static location is still not enough for you? Say, you want to add a feature to your “Best Bars in my Hometown”-App that allows the user to track his way home after his visit to the bar (So he can see the next day how he got home in case he can’t remember…). To test that feature you would have to make the Simulator pretend to be moving from one location to another.

This is where a second GPX datatype comes into play: the time data type. When you add a time element to a waypoint element and you have multiple waypoint elements Xcode simulates a movement between those waypoints:



    
        
    
    
        
    
    
        
    

When you select this GPX file from the list of locations Xcode starts to simulate a movement from the first waypoint to the second waypoint and then to the third.

The speed of this movement is determined by the value of the time elements. The absolute time does not matter. Xcode only uses the time difference between the waypoints to calculate a speed. So in my example it takes the Simulator 1 minute to “move” from the first waypoint to the second waypoint and then another minute before it reaches the third waypoint.

Really cool!

Set a UIWebView’s height to the height of its HTML content using Auto Layout

Posted: | Author: | Filed under: iOS, Swift | Tags: , | 11 Comments »

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:

  1. Connect the UIWebView from the nib to an outlet in your view controller.
  2. Disable scrolling in the UIWebView.
  3. 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:

    0FJCG

  4. Connect the UIWebView‘s height constraint to an outlet in your view controller.
  5. Set the view controller as UIWebViewDelegate.
  6. In webViewDidFinishLoad set the height constraint’s constant to the height of the contentSize of the scroll view inside the web view.
  7. 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.

Here is the code that you need:

import UIKit

var MyObservationContext = 0

class ViewController: UIViewController {

    @IBOutlet weak var webview: UIWebView!
    @IBOutlet weak var webviewHeightConstraint: NSLayoutConstraint!
    
    var observing = false
    
    override func viewDidLoad() {
        super.viewDidLoad()
        webview.scrollView.isScrollEnabled = false
        webview.delegate = self
        if let url = URL(string: "https://www.google.de/intl/de/policies/terms/regional.html") {
            webview.loadRequest(URLRequest(url: url))
        }
    }
    
    deinit {
        stopObservingHeight()
    }
    
    func startObservingHeight() {
        let options = NSKeyValueObservingOptions([.new])
        webview.scrollView.addObserver(self, forKeyPath: "contentSize", options: options, context: &MyObservationContext)
        observing = true;
    }
    
    func stopObservingHeight() {
        webview.scrollView.removeObserver(self, forKeyPath: "contentSize", context: &MyObservationContext)
        observing = false
    }
    
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        guard let keyPath = keyPath else {
            super.observeValue(forKeyPath: nil, of: object, change: change, context: context)
            return
        }
        switch keyPath {
        case "contentSize":
            if context == &MyObservationContext {
                webviewHeightConstraint.constant = webview.scrollView.contentSize.height
            }
        default:
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
        }
    }
}

extension ViewController: UIWebViewDelegate {
    func webViewDidFinishLoad(_ webView: UIWebView) {
        webviewHeightConstraint.constant = webview.scrollView.contentSize.height
        if (!observing) {
            startObservingHeight()
        }
    }
}

In case you have difficulties implementing this, i put a working example on github

EDIT

GitHub user kenji21 implemented this cool UIWebView subclass that get’s the job done with less code:

class ContentFittingWebView: UIWebView {
    
    var contentSizeObservationToken: NSKeyValueObservation?
    
    override func awakeFromNib() {
        super.awakeFromNib()
        
        startObservingHeight()
    }

    override var intrinsicContentSize: CGSize {
        return scrollView.contentSize
    }
    
    func startObservingHeight() {
        contentSizeObservationToken?.invalidate()
        contentSizeObservationToken = scrollView.observe(\UIScrollView.contentSize, options: [.new], changeHandler: { (scrollView, change) in
            self.invalidateIntrinsicContentSize()
        })
    }
}

How to fade in a UITableView section index when the user scrolls

Posted: | Author: | Filed under: iOS, Swift | Tags: | No Comments »

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:

func scrollViewDidScroll(scrollView: UIScrollView) {
    let fadeInDistance: CGFloat = 100
    var alpha: CGFloat = 1
    if scrollView.contentOffset.y < 1 {
        alpha = 0
    } else if scrollView.contentOffset.y >= 1 && scrollView.contentOffset.y < fadeInDistance {
        alpha = scrollView.contentOffset.y / fadeInDistance
    }
    tableView.sectionIndexColor = UIColor(red: 0, green: 0, blue: 0, alpha: alpha)
    tableView.sectionIndexBackgroundColor = UIColor(red: 1, green: 1, blue: 1, alpha: alpha)
}

To initially hide the section index you have to set those two colors to UIColor.clearColor() in viewDidLoad:

func viewDidLoad() {
   super.viewDidLoad()
   tableView.sectionIndexColor = UIColor.clearColor()
   tableView.sectionIndexBackgroundColor = UIColor.clearColor()
}

UIScrollView and Auto Layout.

Posted: | Author: | Filed under: iOS, Swift | Tags: , , | 9 Comments »

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:

scrollview_al

If you are using Masonry or SnapKit here is a code example on how to set the constraints programmatically:

topView.snp_makeConstraints { (make) -> Void in
    make.top.equalTo(0)
    make.left.equalTo(0)
    make.width.equalTo(scrollView)
}
label1.snp_makeConstraints { (make) -> Void in
    make.top.equalTo(topView.snp_bottom)
    make.left.equalTo(0)
    make.width.equalTo(scrollView)
}
label2.snp_makeConstraints { (make) -> Void in
   make.top.equalTo(label1.snp_bottom)
   make.left.equalTo(0)
   make.width.equalTo(scrollView)
}
label3.snp_makeConstraints { (make) -> Void in
    make.top.equalTo(label2.snp_bottom)
    make.left.equalTo(0)
    make.width.equalTo(scrollView)
    make.bottom.equalTo(0)
}

iOS9 In-App Purchase bug when user cancels the purchase process

Posted: | Author: | Filed under: iOS | Tags: , , | No Comments »

I just stumbled upon a bug that seems to be introduced in iOS9. When a user tries to buy a product via In-App Purchase and then cancels the process when he is asked if he really wants to buy the product a wrong error type is returned by Apple.

When the user presses “Cancel” in the UIAlertView the SKPaymentTransactionObserver method paymentQueue:updatedTransactions: gets called. The cancellation is handled like an error with the error type SKErrorPaymentCancelled. You can check for this error type because you don’t really want to show an error page when the user cancelled the purchase process. It’s not really an error but the user’s free will. So you only show an error message for the other error types:

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
    for (SKPaymentTransaction *transaction in transactions) {
        switch (transaction.transactionState) {
            case SKPaymentTransactionStateFailed:
                if (transaction.error.code == SKErrorPaymentCancelled) {
                   // just cancel the purchase process without
                   // showing an error message
                } else {
                   // show an error message
                }
                break;
            default:
                break;
        }
    }
}

So far, so good. Unfortunately under iOS9 when the user cancels the error type SKErrorUnknown is returned from Apple. That is unfortunate as it results in an error message being displayed when the user pressed “Cancel”. Somehow the correct error type SKErrorPaymentCancelled got lost in iOS9.

That is unfortunate, because we really don’t want to show the error message when the user cancels the purchase process. I do not see a solution for this problem as we cannot suppress all error messages of type SKErrorUnknown.

I filed a radar. Would be nice if Apple would fix that soon. Well, we know how seriously Apple treats radars, but hey, one can dream…

UPDATE: This is now fixed with the iOS9 release version

Use the Nil Coalescing Operator in Swift to assign default values to properties

Posted: | Author: | Filed under: iOS, Objective-C, Swift | No Comments »

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:

- (void)setupBackgroundColor:(UIColor *)color {
   self.backgroundColor = color ? : [UIColor whiteColor];
}

How to show your iOS Share Extension only in Safari (or other browsers)

Posted: | Author: | Filed under: iOS | Tags: , | 8 Comments »

When you develop a Share Extension for your iOS App many times the Extension is not suitable for all places where it could be shown to the user. For example if your Share Extension can only share URLs it does not make sense to offer it to the user when he is using the Photos App.

This is why Apple implemented the NSExtensionActivationRule field in the Extension’s Info.plist. That’s quite straightforward and not exactly rocket science, but still many tutorials on Extension do not mention this and it took me a while to find out how this works, so I’ll share this with you.

So, when you create a new Extension Xcode creates a Info.plist in the ‘YOUR_EXTENSION/Supporting Files’ folder. In there you’ll find a Dictionary with the key NSExtension.

NSExtension

    NSExtensionAttributes
    
        NSExtensionActivationRule
        TRUEPREDICATE
    
    NSExtensionMainStoryboard
    MainInterface
    NSExtensionPointIdentifier
    com.apple.share-services

The interesting part is the String with the key NSExtensionActivationRule. By default Xcode sets its value to TRUEPREDICATE. That’s just for development purposes and makes sure that your Extension is shown as a sharing option everywhere. This has to be removed before you submit your app the the AppStore of your app will be rejected.

So in our case we can only share an URL so we want to make sure that it does not get added to the sharing options in Apps that do not offer an URL to share (like the Photos App).

So you have to change the type of the NSExtensionActivationRule field from String to Dictionary and add one or more keys that Apple provides to define what data types your Extension supports:

NSExtensionActivationSupportsAttachmentsWithMaxCount
NSExtensionActivationSupportsAttachmentsWithMinCount
NSExtensionActivationSupportsFileWithMaxCount
NSExtensionActivationSupportsImageWithMaxCount
NSExtensionActivationSupportsMovieWithMaxCount
NSExtensionActivationSupportsText
NSExtensionActivationSupportsWebURLWithMaxCount
NSExtensionActivationSupportsWebPageWithMaxCount

In our case we only support URLs so we need the NSExtensionActivationSupportsWebURLWithMaxCount key. So the NSExtension Dictionary should look like this:

NSExtension

    NSExtensionAttributes
    
        NSExtensionActivationRule
	
	    NSExtensionActivationSupportsWebURLWithMaxCount
	    1
	
    
    NSExtensionMainStoryboard
    MainInterface
    NSExtensionPointIdentifier
    com.apple.share-services

And now your Share Extension is not activated in the Photo App anymore. Only in apps that offer a URL will now show your Share Extension as one of the sharing options.

How to get rid of the padding / insets in an UITextView

Posted: | Author: | Filed under: iOS | Tags: | 15 Comments »

Sometimes you want to display some text that contains an email address, an URL, a phone number, an address or a date. Of course you want to make these text parts interactive so that a user can tap on an URL and directly go to the linked webpage, or make a phone call when he taps on a phone number, etc.

Implementing that is really easy. You just use an UITextView and set dataDetectorTypes = .all. And voilá: the UITextView now automatically detects phone numbers, links, calendar events and addresses and makes them tappable. Really nice!

However, there is one caveat: UITextView adds some insets around the text it displays. So if you want to align an UITextView with another UI element (like an UILabel) and you set both elements to the same origin.x, you will see that the text of the UILabel and the text of the UITextView are not aligned. The text of the UITextView is shifted a bit to the right. If you try to align them horizontally you will notice that it is also shifted a bit to the bottom. When you add background color to the UITextView you see that it adds some padding around it’s text. That is nice, if your UITextView has a border or a background color, or you use it to display some editable or scrollable text, but in our case it is not ideal.

Luckily there is a solution for that. UITextView has a property called textContainerInset. The default value for this inset is (top = 8, left = 0, bottom = 8, right = 0). So setting this inset to UIEdgeInsetsZero should get rid of the top and the bottom padding.

textView.textContainerInset = .zero

Works as expected. But there is still some padding to the left and the right of the text. The solution to get rid of that is not as obvious as setting the insets to zero. UITextView uses a NSTextContainer object to define the area in which the text is displayed. And this NSTextContainer object has a property called lineFragmentPadding. This property defines the amount by which the text is inset within line fragment rectangles, or in other words: the left and right padding of the text. The default value for this is 5.0, so setting this to 0 removes the padding.

textView.textContainer.lineFragmentPadding = 0

So, now the text in the UITextView aligns beautifully with the text of the UILabel.