Search This Blog

Monday, October 30, 2017

iOS Swift. Swipe gesture recogniser in multiple directions

For swiping in each direction we need to create Swipe gesture recognizer and specify direction that we need for this recognizer. For each direction we need create separate recognizer.
override func viewDidLoad() {
    super.viewDidLoad()
        
    // Swipe Left
    let swipeLeft = UISwipeGestureRecognizer(target: self, action: #selector(swipingLeft))
    swipeLeft.direction = .left
    self.view.addGestureRecognizer(swipeLeft)
        
    // Swipe Right
    let swipeRight = UISwipeGestureRecognizer(target: self, action: #selector(swipingRight))
    swipeRight.direction = .right
    self.view.addGestureRecognizer(swipeRight)
}
    
@objc func swipingLeft(sender: UISwipeGestureRecognizer) {
    print("Swipe Left")
}
    
@objc func swipingRight(sender: UISwipeGestureRecognizer) {
    print("Swipe Right")
}

We can make code more universal. When we swiping in different directions we can handle all this swipes in one method and switch between directions.
override func viewDidLoad() {
    super.viewDidLoad()
        
    let swipeRight = UISwipeGestureRecognizer(target: self,
                                              action: #selector(self.handleSwipe))
    swipeRight.direction = UISwipeGestureRecognizerDirection.right
    self.view.addGestureRecognizer(swipeRight)
        
    let swipeDown = UISwipeGestureRecognizer(target: self,
                                             action: #selector(self.handleSwipe))
    swipeDown.direction = UISwipeGestureRecognizerDirection.down
    self.view.addGestureRecognizer(swipeDown)
}
    
@objc func handleSwipe(gesture: UIGestureRecognizer) {
    if let swipeGesture = gesture as? UISwipeGestureRecognizer {
        switch swipeGesture.direction {
        case UISwipeGestureRecognizerDirection.right:
            print("Swiped right")
        case UISwipeGestureRecognizerDirection.down:
            print("Swiped down")
        case UISwipeGestureRecognizerDirection.left:
            print("Swiped left")
        case UISwipeGestureRecognizerDirection.up:
            print("Swiped up")
         default:
            break
        }
    }
}

iOS Swift. How to detect double OR triple tap with UITapGestureRecognizer

For detecting touches in iOS application we use UITapGestureRecognizer. When we need react only on special number of touches, for example two or three, we must simply specify this parameter for gesture recognizer object.

When number of touches is 3
let tapGestureRecognizer = UITapGestureRecognizer(target: self,
                                                          action: #selector(imageTappedTriple))
tapGestureRecognizer.numberOfTapsRequired = 3

When number of touches is 2
let tapGestureRecognizer = UITapGestureRecognizer(target: self,
                                                          action: #selector(imageTappedDouble))
tapGestureRecognizer.numberOfTapsRequired = 2

Important to remember is make enable image for interaction with user.
self.imageView.isUserInteractionEnabled = true

Full code.
import UIKit

class ViewController: UIViewController {
    
    @IBOutlet var imageView: UIImageView!

    override func viewDidLoad() {
        super.viewDidLoad()
        let tapGestureRecognizer = UITapGestureRecognizer(target: self,
                                                          action: #selector(imageTappedTriple))
        tapGestureRecognizer.numberOfTapsRequired = 3
        self.imageView.isUserInteractionEnabled = true
        self.imageView.addGestureRecognizer(tapGestureRecognizer)
    }
    
    @objc func imageTappedTriple(sender: UITapGestureRecognizer) {
        print("Image was tapped three times!")
    }

}

Sunday, October 29, 2017

How to add a border just to the one side of a UIView

We need to add border to UIView. Let's create view and place it in the center of screen with NSLayoutConstraint
var containerView: UIView!

override func viewDidLoad() {
    super.viewDidLoad()
    self.containerView = UIView()
    self.containerView.backgroundColor = UIColor.red
    self.view.addSubview(containerView)
    self.containerView.translatesAutoresizingMaskIntoConstraints = false
    NSLayoutConstraint.activate([
        self.containerView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
        self.containerView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
        self.containerView.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 0.3),
        self.containerView.heightAnchor.constraint(equalTo: self.view.heightAnchor, multiplier: 0.3)
    ])
}


Border using SubView


In viewDidAppear method add top border to view using addSubView method.
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    let topBorderView = UIView(frame: CGRect(x: 0, y: 0,
                                                 width: self.containerView.frame.size.width,
                                                 height: 10))
    topBorderView.backgroundColor = UIColor.black
    self.containerView.addSubview(topBorderView)
}
Great, top border is added. But is it adaptive, what happen when parent view frame is changed. Let's see.


When parent view frame changed top border is not changed it's size.



What can go wrong when working with NSLayoutConstraint

Often when working with AutoLayout programmatically something can go wrong. Constraints that you created just not work or program fails. There are most popular mistakes that can happen

1. Forgot to addSubView method

You simply forgot to add your view as subview to parent view.
let containerView = UIView()
self.view.addSubview(containerView)

2. Forgot to set translatesAutoresizingMaskIntoConstraints property to false

Another important thing to remember. You need to enable AutoLayot for your view. So, if you forgot to set translatesAutoresizingMaskIntoConstraints to false then your view will not using AutoLayout.
let containerView = UIView()
containerView.translatesAutoresizingMaskIntoConstraints = false

3. Forgot to activate constraint

When you create new constraint it is not enough to make it work. You have to activate it.
containerView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true

4. Your views in different hierarchies

You can only add constraint to views that are in the same hierarchy. In other words - constraint for your view must operate with parent view (or view from the same hierarchy).

5. You must specify position and size both. Directly or indirectly

When you create constraints you need to specify size and position for your view. In other case your constraints will not work.

Directly specifying example (setup center position and width and height)
containerView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
containerView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
containerView.widthAnchor.constraint(equalToConstant: 200).isActive = true
containerView.heightAnchor.constraint(equalToConstant: 200).isActive = true

Indirectly specifying example (setup top, bottom, leading and width)
containerView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
containerView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
containerView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
containerView.widthAnchor.constraint(equalToConstant: 25).isActive = true

Monday, October 23, 2017

iOS Swift. How to Animate a Bar Button Item

Today I will show you how to make animated bar buttons and make it with coding. First of all we need icons for our navigation bar. Let's get them from google material design icons.

Google Material Design Icons

Adding icons to project


The only thing that we make with Storyboard is embed our view controller in navigation controller. We need it for navigation bar as container for bar buttons.


Declare two bar button items
var settingsBarButton: UIBarButtonItem?
    
var favoriteBarButton: UIBarButtonItem?

And Bool variable for switch icon of favourite button.
var favorite: Bool = false

Creating images from Assets. One image for settings icon and two images for favourite icon (favourite and unfavourite).
let settingsImage = UIImage(named: "ic_settings_48pt")?.withRenderingMode(.alwaysTemplate)
let favoriteBorderImage = UIImage(named: "ic_favorite_border_48pt")?.withRenderingMode(.alwaysTemplate)
let favoriteFullImage = UIImage(named: "ic_favorite_48pt")?.withRenderingMode(.alwaysTemplate)

Inside viewDidLoad method let's create UIButton, configure it and initialize settings bar button item with this UIButton. We put it to the left part of navigation item of view controller.
let settingsButton = UIButton(type: .system)
settingsButton.tintColor = .black
settingsButton.setImage(self.settingsImage, for: .normal)
settingsButton.frame = CGRect(x: 0, y: 0, width: 30, height: 30)
settingsButton.addTarget(self, action: #selector(settingsButtonTapped), for: .touchUpInside)
self.settingsBarButton = UIBarButtonItem(customView: settingsButton)
self.navigationItem.setLeftBarButton(settingsBarButton, animated: false)

It is touch handler for button. Here we make animation. There are two steps:

  1. Rotate view to some angle
  2. With animation we restore initial view rotation as it was before first rotation

@objc func settingsButtonTapped(_ sender: UIButton) {
    self.settingsBarButton?.customView?.transform =
        CGAffineTransform(rotationAngle: CGFloat(CGFloat.pi * -3/4))
    UIView.animate(withDuration: 0.8) {
        self.settingsBarButton?.customView?.transform = .identity
    }
}

The same for favourite bar button. Initially we create it with favourite empty icon (unfavourite). We put it to the right part of navigation item of view controller.
let favoriteButton = UIButton(type: .system)
favoriteButton.tintColor = .black
favoriteButton.setImage(self.favoriteBorderImage, for: .normal)
favoriteButton.frame = CGRect(x: 0, y: 0, width: 30, height: 30)
favoriteButton.addTarget(self, action: #selector(favoriteButtonTapped), for: .touchUpInside)
self.favoriteBarButton = UIBarButtonItem(customView: favoriteButton)
self.navigationItem.setRightBarButton(favoriteBarButton, animated: false)

Touch handler for favourite button. The same logic as before, but here we use other type of transformation(previously we use rotation), now we use scaling transformation:
  1. Change button scale.
  2. Restore button scale with animation. Also inside animation we change icon of button. 
Here we use spring animation. It means that here will be appear special effects during animations. Such as velocity and damping changing and etc.
@objc func favoriteButtonTapped(_ sender: UIButton) {
        self.favoriteBarButton?.customView?.transform = CGAffineTransform(scaleX: 0, y: 0)
        UIView.animate(withDuration: 0.5,
                       delay: 0.0,
                       usingSpringWithDamping: 0.6,
                       initialSpringVelocity: 10,
                       options: .curveEaseInOut,
                       animations: {
                        self.favorite = !self.favorite
                        let image = self.favorite ? self.favoriteFullImage : self.favoriteBorderImage
                        if let button = self.favoriteBarButton?.customView as? UIButton {
                            button.setImage(image, for: .normal)
                        }
                        self.favoriteBarButton?.customView?.transform = .identity
        }, completion: nil)
}

How it all work





GitHub Link

Sunday, October 22, 2017

How to remove all subviews of a view in Swift?

Following task - remove all subViews of current view. Let's see how we can implement this.

For example we create UIView and add UILabel to it.
var containerView = UIView()
containerView.addSubview(UILabel(frame: CGRect.zero))

1. Simple For Loop
for subView in containerView.subviews {
    subView.removeFromSuperview()
}

2. ForEach Loop
containerView.subviews.forEach {
    $0.removeFromSuperview()
}

3. Using Map Function
containerView.subviews.map { $0.removeFromSuperview() }

4. Making Extension

If we want to make removing enable for all views in project then let's create Extension.
extension UIView {   
    func removeAllSubView() {
        self.subviews.forEach { $0.removeFromSuperview() }
    }
}

containerView.removeAllSubView()

5. Going Universal with Generics

We can go further and make our Extension universal with Generics. Now You can specify which type of subViews You want to remove.
extension UIView {
    
    func removeAllSubViewOfType<T: UIView>(type: T.Type) {
        self.subviews.forEach {
            if ($0 is T) {
                $0.removeFromSuperview()
            }
        }
    }
}

containerView.removeAllSubViewOfType(type: UILabel.self)

6. The Same but Using Filter and Map Functions

The same as above. But here we use chain of High-order functions.
extension UIView {
    
    func removeAllSubViewOfTypeUsingHOF<T: UIView>(type: T.Type) {
        self.subviews.filter({ $0 is T }).map({ $0.removeFromSuperview() })
    }
}

containerView.removeAllSubViewOfTypeUsingHOF(type: UILabel.self)

Thursday, October 19, 2017

Flat nested array recursively in Swift

Following task - make flat array from nested array. Because there are could be any levels of nested array we use recursive approach.

For example - given array [1, [2, [3, 4, 5]]]. We need to get this [1, 2, 3, 4, 5]

Standard Approach

Algorithm
  1. In for loop go through the array
  2. If element of array is int then add it to new array
  3. If element of array is array too then make recursive invoke of function and send this array
We use Any type as type of input array because input array can hold as integers as array. They both are value types, so Any is suitable for us in this function.

Let's get coding.
import UIKit

let array: [Any] = [1, 2, [3]]

func makeFlatArray(_ array: [Any]) -> [Int] {
    var flatArray = [Int]()
    for item in array {
        if let item = item as? Int {
            flatArray.append(item)
        } else if let item = item as? [Any] {
            let result = makeFlatArray(item)
            flatArray += result
        }
    }
    return flatArray
}

print(makeFlatArray([1, 2, 3]))
// [1, 2, 3]
print(makeFlatArray([1, [2, 3, 4]]))
// [1, 2, 3, 4]
print(makeFlatArray([1, [2, [3, 4]]]))
// [1, 2, 3, 4]
print(makeFlatArray([[1], [2, [3, 4, [5]]]]))
// [1, 2, 3, 4, 5]

Using Generics

What if we want to use this function with array of any type. We should use Generics:
func makeFlatArrayGeneric<T>(_ array: [Any]) -> [T] {
    var flatArray = [T]()
    for item in array {
        if let item = item as? T {
            flatArray.append(item)
        } else if let item = item as? [Any] {
            let result: [T] = makeFlatArrayGeneric(item)
            flatArray += result
        }
    }
    return flatArray
}

Now we can use this function with any kind of array. With integer array as previous
let array: [Any] = [1, 2, [3], [4, [5]]]
let items: [Int] = makeFlatArrayGeneric(array)
// [1, 2, 3, 4, 5]

And with array of strings
let array: [Any] = ["A", "BB", ["CCC"], ["DD", ["EE"]]]
let items: [String] = makeFlatArrayGeneric(array)
// ["A", "BB", "CCC", "DD", "EE"]

Make Extension for Array 

What if we want this feature for any array as built in method. Let's create extension for Array Type. Here we use Swift method flatMap.
extension Array {
    
    func makeFlat() -> [Element] {
        let flatArray = self.flatMap { (element) -> [Element] in
            if let array = element as? Array {
                return array.makeFlat()
            }
            return [element]
        }
        return flatArray
    }
}

Result of using array extension.
let array: [Any] = ["A", "BB", ["CCC"], ["DD", ["EE"]]]
print(array.makeFlat())
// ["A", "BB", "CCC", "DD", "EE"]

Tuesday, October 17, 2017

Reverse every even or every odd word in string in swift

Following task - reverse every even or every odd word in sentence.

Algorithm

  1. Get array of words from string (sentence)
  2. Create new empty string
  3. In for loop go through the array and according to condition reverse word or not and append it to new string
Let's get implemented our algorithm.
import UIKit

let str = "This is the string which will be reverted particaually"

enum WhichNumer: Int {
    case Even = 0
    case Odd = 1
}

func reverseWords(str: String, number: WhichNumer = .Even) -> String {
    let array = str.components(separatedBy: " ")
    var newStr = ""
    for i in 0..<array.count {
        if newStr != "" {
            newStr += " "
        }
        let word = array[i]
        if (i + 1) % 2 == number.rawValue {
            newStr += String(word.reversed())
        } else {
            newStr += word
        }
    }
    
    return newStr
}

print(reverseWords(str: str))
// This si the gnirts which lliw be detrever particaually

print(reverseWords(str: str, number: .Odd))
//sihT is eht string hcihw will eb reverted yllauacitrap

But we can improve our code with using High-ordered function. 

There is following algorithm:

1. Create array of string from initial string
str.components(separatedBy: " ")
// ["This", "is", "the", "string", "which", "will", "be", "reverted", "particaually"]

2. Convert this array to enumerated. According to Apple Documentation after this we can get sequence of pairs with index and value.
"Returns a sequence of pairs (n, x), where n represents a consecutive integer starting at zero, and x represents an element of the sequence."
str.components(separatedBy: " ").enumerated()
//    0 : This
//    1 : is
//    2 : the
//    3 : string
//    4 : which
//    5 : will
//    6 : be
//    7 : reverted
//    8 : particaually

3. Now we can use map function to this array of pairs. We check index and modify value if needed.
str.components(separatedBy: " ").enumerated().map { (index, value) -> String in
    if ((index + 1) % 2 == number.rawValue) {
        return String(value.reversed())
    }
    return value
})
// ["This", "si", "the", "gnirts", "which", "lliw", "be", "detrever", "particaually"]

4. Finally, array of words can be convert to string with joined function.
let newStr = str.components(separatedBy: " ").enumerated().map { (index, value) -> String in
    if ((index + 1) % 2 == number.rawValue) {
        return String(value.reversed())
    }
    return value
}.joined(separator: " ")
// This si the gnirts which lliw be detrever particaually

Here new implementation of our function.
func reverseWordsUsingHOF(str: String, number: WhichNumer = .Even) -> String {
    let newStr = str.components(separatedBy: " ").enumerated().map { (index, value) -> String in
        print(index)
        if ((index + 1) % 2 == number.rawValue) {
            return String(value.reversed())
        }
        return value
    }.joined(separator: " ")
    
    return newStr
}

print(reverseWordsUsingHOF(str: str))
// This si the gnirts which lliw be detrever particaually