Search This Blog

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

Find most common word in array of string using swift

Following task - find most common word in array of string.

Algorithm
  1. Create and fill up dictionary of word frequency. Count for each word in array
  2. Find key with maximum value and return this key.

Let's get implemented this algorithm.
let array = ["This", "Test", "Swift", "Never", "Swift", "Test", "Swift"]

func getMostCommonWord(array: [String]) -> String {
    
    var dict = [String: Int]()
    
    for word in array {
        if let count = dict[word] {
            dict[word] = count + 1
        } else {
            dict[word] = 1
        }
    }
    
    var mostCommonWord = ""
    for key in dict.keys {
        if mostCommonWord == "" {
            mostCommonWord = key
        }
        if let nextCount = dict[key], let prevCount = dict[mostCommonWord] {
            if nextCount > prevCount {
                mostCommonWord = key
            }
        }
    }
    
    return mostCommonWord
}

print(getMostCommonWord(array: array))
// Swift

How we can improve this algorithm. Using High-ordered functions. 

We can sort dictionary. As result of sorting dictionary we get descendingly sorted array of tuples. In this array of tuples first element contain key with maximum count. After that we can get first tuple and get key variable from it.
let sortedTuples = dict.sorted { $0.1 > $1.1}
print(sortedTuples)
// [(key: "Swift", value: 3), (key: "Test", value: 2), (key: "This", value: 1), (key: "Never", value: 1)]
print(sortedTuples.first!)
// (key: "Swift", value: 3)

So now we can rewrite our function.
func getMostCommonWordUsingMap(array: [String]) -> String {
    
    var dict = [String: Int]()
    
    for word in array {
        if let count = dict[word] {
            dict[word] = count + 1
        } else {
            dict[word] = 1
        }
    }
    
    var mostCommonWord = ""
    
    if let first = (dict.sorted { $0.1 > $1.1}).first {
        mostCommonWord = first.key
    }
    
    return mostCommonWord
}

print(getMostCommonWordUsingMap(array: array))
// Swift

Saturday, October 14, 2017

iOS. Any vs AnyObject difference in Swift

Any and AnyObjects are not exactly data types. They are type alias.  Let's see Apple Documentation.

Swift provides two special types for working with nonspecific types:
  • Any can represent an instance of any type at all, including function types.
  • AnyObject can represent an instance of any class type.

Any Usage


Let's create array of Any and add items to that.
import UIKit

var items = [Any]()
items.append(1)
items.append("Special")
items.append(3.5)
items.append((55, 22))
items.append([1, 2, 3, 4])

func sum(_ a: Int, _ b: Int) -> Int {
    return (a + b)
}
let aSum = sum
items.append(aSum)

class Vehicle {
    var name: String
    init(_ name: String) {
        self.name = name
    }
}
items.append(Vehicle("MyCar"))

for item in items {
    print(item)
}

//1
//Special
//3.5
//(55, 22)
//[1, 2, 3, 4]
//(Function)
//__lldb_expr_102.Vehicle

It works fine because item of Any type can be added to array.

AnyObject Usage


Let's see how works AnyObject type. Here we create two classes and add their instances to array, works fine.
class Device {}
class Vehicle {}

var anyObjects: [AnyObject] = [Device(), Vehicle()]
// it works fine, because there only reference type instances - classes

But if we try to add non class types to AnyObject array we got error. Because Int and String are Structs in Swift, but Struct is value type.
var anyObjects: [AnyObject] = [1, "Test"]
// error: value of type 'Int' does not conform to expected element type 'AnyObject'