Utilizando o Zeplin para ler designs desenvolvidos no Sketch

Utilizando o Zeplin para ler designs desenvolvidos no Sketch

Construir uma tela no Xcode a partir de um projeto do Sketch pode ser uma tarefa bem trabalhosa. Caso o projeto tenha sido impresso não é possível obter as medidas que foram utilizadas pelo designer. Cabe ao desenvolvedor tentar vários valores diferentes para chegar a um resultado aproximado do design. Mesmo que a equipe de desenvolvimento tenha licenças do Sketch e possa abrir o projeto, ainda assim, terão um pouco de trabalho para ver os espaços entre os componentes. O Zeplin veio como uma ferramenta que preenche este espaço entre o design do projeto e o desenvolvimento do produto.

O Zeplin permite que o desenvolvedor clique em um item do design e consiga ver todas as medidas necessárias para a construção do elemento. As medidas já estão no formato utilizado pelo Xcode. Além das medidas do elemento é possível ver as medidas em relação aos outros itens que estão em volta do elemento. Também é possível ver detalhes como o código das cores e os dados das fontes utilizadas pelo elemento selecionado.

Todas as cores e fontes utilizadas no projeto podem ser exportadas para código Swift ou Objective-C e incorporadas ao projeto. Ao invés de criar a cor informando o código RGB no UIColor basta chamar o nome da cor gerada pelo Zeplin. Os assets também podem ser baixados pelo Zeplin permitindo que o desenvolvedor não dependa do designer para esta tarefa.

Visualisando o Design no Zeplin

Para exemplificar a utilização destas ferramentas irei utilizar o projeto do Rafael Conde disponível no GitHub.

Twitter for iOS

O Sketch possui uma versão de avaliação que pode ser baixada no site. O Zeplin é grátis com a limitação de um projeto mas sem limitação de usuários. Ele pode ser baixado aqui.

Abra o projeto no Sketch, selecione o artboard e envie o mesmo para o Zeplin através do menu Plugins => Zeplin => Export Selected Artboards… ⌘E.

Export From Sketch

O artboard ficará disponível no Dashboard do Zeplin. Ele possui duas telas principais no projeto. O Dashboard (⌘D) e o Styleguide (⌘G).

No Dashboard você poderá visualizar todo o layaute e as propriedades de cada componente do layout.Dashboard

 

No Styleguide ficam os recursos que serão exportados para o Xcode como fontes e cores. Veremos a seguir como selecionar estes recursos.Styleguide

 

No Dashboard vamos selecionar o nome do usuário do segundo tweet “Myke Hurley”. Ao selecioná-lo no lado esquerdo será apresentado as propriedades do label como tamanho, fonte, cor, posição, etc.

Properties

Ao lado da fonte e da cor temos botões para inserir estes dados no Styleguide. Então você terá que selecionar todas as fontes e cores que serão exportadas para o Xcode.

Seguindo com o label do usuário podemos ver a distancia para os outros componentes ao passar o mouse sobre eles e mantendo o label selecionado. Vamos verificar as distancias top, leading, bottom e trealing.

TopAnchorLeadingAnchorBottomAnchorTrailingAnchor

Para baixar os assets basta selecioná-lo e depois clicar na botão na barra de propriedades.

Asset SelectedAssets Download

Gerando os Tweets

Para testar o nosso projeto iremos utilizar uma resposta da API home_timeline do Twitter. Como o foco deste artigo é mostrar a criação da view, iremos obter a resposta através do console disponível no apigee. Acesse este endereço para abrir o console.

Selecione https://api.twitter.com/1.1 como Service e Oauth 1 como Authentication. Será aberta uma tela solicitando a permissão de acesso ao Twitter. Logue na sua conta e autorize o aplicativo.

Request Twitter Permission

No menu lateral esquerdo, selecione o método /statuses/home_timeline.json.

Select an API method

Nos parâmetros da consulta coloque 50 no count. Clique em Send.

Query parameter

Será retornado um Response com o código 200 e mais abaixo teremos o json com o resultado.

Response

Copie o json e cole em um arquivo. Você pode utilizar o site Code Beautify para formatar o json antes de colar no arquivo.

OBS: Talvez você tenha que substituir as strings "<a href=" por "<a href=\" e " rel="nofollow"> por \" rel=\"nofollow\">.

Eu salvei uma versão formatada com o nome Tweets.json e uma versão compacta com o nome Tweets.min.json.

Criando o Projeto

Crie um novo projeto iOS no Xcode. Escolha o template Single View Application. Dê o nome de TwitterClone e escolha a linguagem Swift.

Recentemente tenho visto muitas discussões sobre a utilização de Storyboard e xib versus construir a interface via código. Sempre utilizei storyboard desde que comecei a desenvolver para iOS. Na minha opinião o grande problema em utilizar storyboards é o controle de versão do xml gerado. Em equipes com muitos analistas o gerenciamento de conflitos torna-se um pesadelo para o desenvolvedores. Em função disto, comecei recentemente a criar interfaces utilizando apenas código para ver como seria minha adaptação. Recomendo o site Lets Build That App do Brian Voong que sempre monta os componentes via código em seus tutoriais.

Neste projeto não utilizaremos storyboards. Criaremos toda a interface via código.

Exclua o arquivo Main.storyboard do projeto. Limpe o campo Main Interface da guia General do projeto.
Deployment Info

Importando os assets, fontes e cores do Zeplin

Exporte os dez assets do projeto para o Xcode. Temos três na navigationBar que nomeei de Add Contact, Search e Compose Tweet. Quatro na tabBar que nomeei de Home, Notifications, Messages e Me.E três abaixo do texto do tweet que nomeei de Reply, Retweet e Favorite.

Assets

Também precisamos selecionar as cores utilizadas no design e clicar no botão como explicado anteriormente. Selecionei as seguintes cores: darkSkyBlue, cloudyBlue, blueyGrey, charcoalGrey, paleGrey e windowsBlue.
Color Pallete
Para as fontes selecionei o título da navigationBar e nomeei de titleMenubarFont, o label com o nome do usuário do tweet que nomeei de nameFont, o texto do tweet que nomeei de textFont, o tempo passado desde a criação do tweet que nomeei de timeLabelFont e o tótulo da tabBar que nomeei de tabBarItem.
Font Book

Criando os modelos

Criei algumas structs para servir de modelo para os dados do Twitter. São structs simples com apenas parte dos dados retornado pela API para que possamos montar a nossa tela. Implementei o método load(fromJson:) -> Self? para carregar os dados do JSON.

Tweet

import Foundation

struct Tweet {
  let id: Int
  let text: String
  let created_at: Date
  let favorited: Bool
  let retweet_count: Int
  let retweeted: Bool
  let favorite_count: Int
  let user: User?
  let entities: TwitterEntity?
  let extendedEntities: TwitterExtendedEntity?
}

extension Tweet {
  static func load(fromJson json: JSON) -> Tweet? {
    guard let id = json["id"] as? Int else { return nil }
    guard let text = json["text"] as? String else { return nil }
    guard let created_at_string = json["created_at"] as? String, let created_at = dateFormatter.date(from: created_at_string) else { return nil }
    guard let favorited = json["favorited"] as? Bool else { return nil }
    guard let retweet_count = json["retweet_count"] as? Int else { return nil }
    guard let retweeted = json["retweeted"] as? Bool else { return nil }
    guard let favorite_count = json["favorite_count"] as? Int else { return nil }
    guard let userJson = json["user"] as? JSON else { return nil }
    guard let entitiesJson = json["entities"] as? JSON else { return nil }

    let extendedEntitiesJson: JSON? = json["extended_entities"] as? JSON

    return Tweet(id: id, text: text, created_at: created_at, favorited: favorited, retweet_count: retweet_count, retweeted: retweeted, favorite_count: favorite_count, user: User.load(fromJson: userJson), entities: TwitterEntity.load(fromJson: entitiesJson), extendedEntities: TwitterExtendedEntity.load(fromJson: extendedEntitiesJson))
  }
}

extension Tweet: Equatable {
  static func == (lhs: Tweet, rhs: Tweet) -> Bool {
    return lhs.id == rhs.id
  }
}

TwitterEntity

import Foundation

struct TwitterEntity {
  let hashtags: [TwitterHashtag]
  let mentions: [TwitterMention]
  let urls: [TwitterURL]
  let medias: [TwitterMedia]
}

extension TwitterEntity {
  static func load(fromJson json: JSON) -> TwitterEntity? {
    var hashtags = [TwitterHashtag]()
    var mentions = [TwitterMention]()
    var urls = [TwitterURL]()
    var medias = [TwitterMedia]()

    if let hashtagsJson = json["hashtags"] as? [JSON] {
      for hashtagJson in hashtagsJson {
        if let hashtag = TwitterHashtag.load(fromJson: hashtagJson) {
          hashtags.append(hashtag)
        }
      }
    }

    if let mentionsJson = json["user_mentions"] as? [JSON] {
      for mentionJson in mentionsJson {
        if let mention = TwitterMention.load(fromJson: mentionJson) {
          mentions.append(mention)
        }
      }
    }

    if let urlsJson = json["urls"] as? [JSON] {
      for urlJson in urlsJson {
        if let url = TwitterURL.load(fromJson: urlJson) {
          urls.append(url)
        }
      }
    }

    if let mediasJson = json["media"] as? [JSON] {
      for mediaJson in mediasJson {
        if let media = TwitterMedia.load(fromJson: mediaJson) {
          medias.append(media)
        }
      }
    }

    return TwitterEntity(hashtags: hashtags, mentions: mentions, urls: urls, medias: medias)
  }
}

extension TwitterEntity: Equatable {
  static func == (lhs: TwitterEntity, rhs: TwitterEntity) -> Bool {
    return lhs.hashtags == rhs.hashtags && lhs.mentions == rhs.mentions && lhs.urls == rhs.urls && lhs.medias == rhs.medias
  }
}

TwitterExtendedEntity

import Foundation

struct TwitterExtendedEntity {
  let photos: [TwitterMedia]
}

extension TwitterExtendedEntity {
  static func load(fromJson json: JSON?) -> TwitterExtendedEntity? {
    var photos = [TwitterMedia]()

    if let mediasJson = json?["media"] as? [JSON] {
      for mediaJson in mediasJson {
        if let type = mediaJson["type"] as? String, type == "photo" {
          if let media = TwitterMedia.load(fromJson: mediaJson) {
            photos.append(media)
          }
        }
      }
    }

    return TwitterExtendedEntity(photos: photos)
  }
}

extension TwitterExtendedEntity: Equatable {
  static func == (lhs: TwitterExtendedEntity, rhs: TwitterExtendedEntity) -> Bool {
    return lhs.photos == rhs.photos
  }
}

TwitterHashtag

import Foundation

struct TwitterHashtag {
  let text: String
}

extension TwitterHashtag {
  static func load(fromJson json: JSON) -> TwitterHashtag? {
    guard let text = json["text"] as? String else { return nil }

    return TwitterHashtag(text: text)
  }
}

extension TwitterHashtag: Equatable {
  static func == (lhs: TwitterHashtag, rhs: TwitterHashtag) -> Bool {
    return lhs.text == rhs.text
  }
}

TwitterMedia

import Foundation

struct TwitterMedia {
  let id: Int
  let url: URL
  let expanded_url: URL
  let display_url: String
  let media_url_https: URL
  let mediaSize: TwitterSize
}

extension TwitterMedia {
  static func load(fromJson json: JSON) -> TwitterMedia? {
    guard let id = json["id"] as? Int else { return nil }
    guard let urlString = json["url"] as? String, let url = URL(string: urlString) else { return nil }
    guard let expanded_urlString = json["expanded_url"] as? String, let expanded_url = URL(string: expanded_urlString) else { return nil }
    guard let display_url = json["display_url"] as? String else { return nil }
    guard let media_url_httpsString = json["media_url_https"] as? String, let media_url_https = URL(string: "\(media_url_httpsString):small") else { return nil }
    guard let sizes = json["sizes"] as? JSON, let small = sizes["small"] as? JSON, let width = small["w"] as? Int, let height = small["h"] as? Int else { return nil }

    let mediaSize = TwitterSize(width: width, height: height)

    return TwitterMedia(id: id, url: url, expanded_url: expanded_url, display_url: display_url, media_url_https: media_url_https, mediaSize: mediaSize)
  }
}

extension TwitterMedia: Equatable {
  static func == (lhs: TwitterMedia, rhs: TwitterMedia) -> Bool {
    return lhs.id == rhs.id
  }
}

TwitterMention

import Foundation

struct TwitterMention {
  let screen_name: String
  let name: String
}

extension TwitterMention {
  static func load(fromJson json: JSON) -> TwitterMention? {
    guard let screen_name = json["screen_name"] as? String else { return nil }
    guard let name = json["name"] as? String else { return nil }

    return TwitterMention(screen_name: screen_name, name: name)
  }
}

extension TwitterMention: Equatable {
  static func == (lhs: TwitterMention, rhs: TwitterMention) -> Bool {
    return lhs.screen_name == rhs.screen_name
  }
}

TwitterURL

import Foundation

struct TwitterURL {
  let url: URL
  let expanded_url: URL
  let display_url: String
}

extension TwitterURL {
  static func load(fromJson json: JSON) -> TwitterURL? {
    guard let urlString = json["url"] as? String, let url = URL(string: urlString) else { return nil }
    guard let expanded_urlString = json["expanded_url"] as? String, let expanded_url = URL(string: expanded_urlString) else { return nil }
    guard let display_url = json["display_url"] as? String else { return nil }

    return TwitterURL(url: url, expanded_url: expanded_url, display_url: display_url)
  }
}

extension TwitterURL: Equatable {
  static func == (lhs: TwitterURL, rhs: TwitterURL) -> Bool {
    return lhs.expanded_url == rhs.expanded_url
  }
}

User

import UIKit

struct User {
  let id: Int
  let name: String
  let screen_name: String
  let profile_image_url_https: URL
}

extension User {
  static func load(fromJson json: JSON) -> User? {
    guard let id = json["id"] as? Int else { return nil }
    guard let name = json["name"] as? String else { return nil }
    guard let screen_name = json["screen_name"] as? String else { return nil }
    guard let profile_image_https = json["profile_image_url_https"] as? String, let profile_image_url_https = URL(string: profile_image_https) else { return nil }

    return User(id: id, name: name, screen_name: screen_name, profile_image_url_https: profile_image_url_https)
  }
}

extension User: Equatable {
  static func == (lhs: User, rhs: User) -> Bool {
    return lhs.id == rhs.id
  }
}

No model utilizamos o tipo TwitterSize que é um alias para o tipo CGSize. Crie um novo arquivo e adicione o código abaixo.

typealias JSON = [String: AnyObject]
typealias TwitterSize = CGSize

let dateFormatter: DateFormatter = {
  var df = DateFormatter()
  df.dateFormat = "ccc MMM dd HH:mm:ss '+'zzzz yyyy"

  return df
}()

Também iremos utilizar o alias JSON que é um dicionário do tipo [String: AnyObject].

Como o objeto DateFormatter é um objeto que iremos utilizar para obter a data de todos os tweets iremos defini-lo globalmente.

Instanciando o controle inicial

Como excluímos o storyboards teremos que criar o controle via código no AppDelegate.swift. Iremos criá-lo no método application(_:didFinishLaunchingWithOptions:).

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
  window = UIWindow(frame: UIScreen.main.bounds)
  window?.makeKeyAndVisible()

  window?.rootViewController = createMainScreen()

  customizeAppearance()

  application.statusBarStyle = .lightContent

  return true
}

Primeiramente criamos um novo UIWindow com o tamanho da tela do device e chamamos o método makeKeyAndVisible(). Depois definimos o controller principal com a propriedade rootViewController.

Em seguida alteramos a aparência de alguns componentes. A propriedade statusBarStyle deixa a statusBar na cor branca.

Crie o método createMainScreen() e adicione o código a seguir.

private func createMainScreen() -> UITabBarController {
  let navigationHomeViewController: UINavigationController = {
    let homeTabBarItem =  UITabBarItem(title: "Timelines", image: #imageLiteral(resourceName: "Home"), tag: 0)

    let homeViewController = HomeViewController()
    homeViewController.tabBarItem = homeTabBarItem

    let navigationHomeViewController = UINavigationController(rootViewController: homeViewController)

    return navigationHomeViewController
  }()

  let navigationNotificationsViewController: UINavigationController = {
    let notificationsTabBarItem = UITabBarItem(title: "Notifications", image: #imageLiteral(resourceName: "Notifications"), tag: 1)

    let notificationsViewController = NotificationsViewController()
    notificationsViewController.tabBarItem = notificationsTabBarItem

    let navigationNotificationsViewController = UINavigationController(rootViewController: notificationsViewController)

    return navigationNotificationsViewController
  }()

  let navigationMessagesViewController: UINavigationController = {
    let messagesTabBarItem = UITabBarItem(title: "Messages", image: #imageLiteral(resourceName: "Messages"), tag: 3)

    let messagesViewController = MessagesViewController()
    messagesViewController.tabBarItem = messagesTabBarItem

    let navigationMessagesViewController = UINavigationController(rootViewController: messagesViewController)

    return navigationMessagesViewController
  }()

  let navigationMeViewController: UINavigationController = {
    let meTabBarItem = UITabBarItem(title: "Me", image: #imageLiteral(resourceName: "Me"), tag: 4)

    let meViewController = MeViewController()
    meViewController.tabBarItem = meTabBarItem

    let navigationMeViewController = UINavigationController(rootViewController: meViewController)

    return navigationMeViewController
  }()

  let mainTabBarController: MainTabBarController = {
    let mainTabBarController = MainTabBarController()

    return mainTabBarController
  }()

  mainTabBarController.addChildViewController(navigationHomeViewController)
  mainTabBarController.addChildViewController(navigationNotificationsViewController)
  mainTabBarController.addChildViewController(navigationMessagesViewController)
  mainTabBarController.addChildViewController(navigationMeViewController)

  mainTabBarController.selectedIndex = 0

  return mainTabBarController
}

Como o nosso aplicativo possui quatro abas criaremos quatro UINavigationController. Para cada UINavigationController temos um bloco onde criamos um UITabBarItem que irá mostrar a descrição e o ícone da aba. Depois instanciarmos um novo objeto UIViewController que será o controller principal da aba. Atribuímos o UITabBarItem a propriedade tabBarItem do controller e criamos o UINavigationController passando o UIViewController para o initializer init(rootViewController:).

A tabBar é criada como um objeto MainTabBarController e recebe os navigationControllers com o método addChildViewController(_:). O método finaliza retornando a tabBar criada.

Agora adicione o método customizeAppearance() para alterarmos a aparência da navigationBar e da tabBar.

func customizeAppearance() {
  let TabBarItemNormalAttributes = [NSFontAttributeName : UIFont.tabBarItemFont(), NSForegroundColorAttributeName: UIColor.blueyGrey]
  let TabBarItemSelectedAttributes = [NSFontAttributeName : UIFont.tabBarItemFont(), NSForegroundColorAttributeName: UIColor.darkSkyBlue]

  UITabBarItem.appearance().setTitleTextAttributes(TabBarItemNormalAttributes, for: .normal)
  UITabBarItem.appearance().setTitleTextAttributes(TabBarItemSelectedAttributes, for: .selected)

  UINavigationBar.appearance().isTranslucent = false
  UINavigationBar.appearance().barTintColor = .darkSkyBlue
  UINavigationBar.appearance().tintColor = .white

  UITabBar.appearance().isTranslucent = false
  UITabBar.appearance().barTintColor = .white
  UITabBar.appearance().tintColor  = .darkSkyBlue
}

Para alterar a aparência e todos os componentes do projeto de uma determinada classe utilizamos o método estático appearance() que retorna um proxy com a aparência da classe.

No nosso caso precisamos alterar as cores da navigationBar e da tabBar de acordo com o design. Também alterei a fonte do tabBarItem.

O nosso código no momento não irá compilar pois precisamos criar os UIViewControllers que serão mostrados nas abas. São eles:

HomeViewController.swift

import UIKit

class HomeViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()

    // Do any additional setup after loading the view.
  }
}

NotificationsViewController.swift

import UIKit

class NotificationsViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()

    // Do any additional setup after loading the view, typically from a nib.
  }
}

MessagesViewController.swift

import UIKit

class MessagesViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()

    // Do any additional setup after loading the view.
  }
}

MeViewController.swift

import UIKit

class MeViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()

    // Do any additional setup after loading the view.
  }
}

Rodando o projeto teremos as abas com uma tela preta pois não criamos os componentes dos controllers.

Screenshot 01

Carregando os Tweets

Para carregar os dados dos tweets iremos criar a classe TwitterClient. Crie o arquivo TwitterClient.swift e adicione o código abaixo.

import UIKit

public class TwitterClient: NSObject {
  public static let sharedInstance: TwitterClient = TwitterClient()

  private override init() {
    super.init()
  }

  func getTweetsFromFile(callback: @escaping ([Tweet]) -> Void) {
    var tweets = [Tweet]()

    DispatchQueue.global(qos: DispatchQoS.QoSClass.background).async {
      if let path = Bundle.main.path(forResource: "Tweets.min", ofType: "json") {
        let url = URL(fileURLWithPath: path)

        do {
          let jsonFile = try Data(contentsOf: url, options: .mappedIfSafe)

          let tweetsJson = try JSONSerialization.jsonObject(with: jsonFile, options: .allowFragments) as! [JSON]

          for tweetJson in tweetsJson {
            if let tweet = Tweet.load(fromJson: tweetJson) {
              tweets.append(tweet)
            }
          }
        } catch {
          dump(error)
        }
      }

      DispatchQueue.main.async {
        callback(tweets)
      }
    }
  }
}

A classe será um singleton com o método getTweetsFromFile(callback:). O método carrega o arquivo Tweets.min.json que possui um array de tweets. Pra cada tweet do arquivo um objeto da classe Tweet será criado com o método load(fromJson:). O resultado é retornado para a closure recebida como parâmetro.

Na classe HomeViewController adicione a propriedade tweets.

var tweets: [Tweet]?

No método viewDidLoad() adicione o seguinte código.

TwitterClient.sharedInstance.getTweetsFromFile() { tweets in
  self.tweets = tweets

  dump(tweets)
}

Rode o projeto e os tweets irão aparecer na janela de log.Log Tweets

Para apresentar os tweets irei utilizar a UICollectionView ao invés do UITableView.

Adicione a seguinte propriedade a classe HomeViewController.

let twitterCellId = "twitterCellId"

let collectionView: UICollectionView = {
  let cv = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
  cv.translatesAutoresizingMaskIntoConstraints = false
  cv.backgroundColor = .white

  return cv
}()

A propriedade collectionView retorna um UICollectionView com o layout do tipo UICollectionViewFlowLayout e com a cor de fundo branca.

No método viewDidLoad() substitua o dump(tweets) por self.collectionView.reloadData() e adicione o seguinte código.

collectionView.dataSource = self
collectionView.delegate = self

collectionView.register(TwitterCell.self, forCellWithReuseIdentifier: twitterCellId)

view.addSubview(collectionView)

collectionView.topAnchor.activeConstraint(equalTo: view.layoutMarginsGuide.topAnchor)
collectionView.trailingAnchor.activeConstraint(equalTo: view.trailingAnchor)
collectionView.bottomAnchor.activeConstraint(equalTo: view.layoutMarginsGuide.bottomAnchor)
collectionView.leadingAnchor.activeConstraint(equalTo: view.leadingAnchor)

No código acima atribuimos o HomeViewController como delegate e datasource do collectionView, registramos uma clase TwitterCell, que criaremos a seguir, adicionamos o collectionView a view do controller e definimos as constraints de autolayout.

Para simplificar a criação das constraints do autolayout eu criei extensões de alguns métodos. Por exemplo:

collectionView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor).isActive = true

ficou

collectionView.topAnchor.activeConstraint(equalTo: view.layoutMarginsGuide.topAnchor)

Crie o arquivo Extensions.swift e adicione o código abaixo.

extension NSLayoutAnchor {
  func activeConstraint(equalTo anchor: NSLayoutAnchor) {
    return self.constraint(equalTo: anchor).isActive = true
  }

  func activeConstraint(equalTo anchor: NSLayoutAnchor, constant c: CGFloat) {
    return self.constraint(equalTo: anchor, constant: c).isActive = true
  }

  func activeConstraint(greaterThanOrEqualTo anchor: NSLayoutAnchor) {
    return self.constraint(greaterThanOrEqualTo: anchor).isActive = true
  }

  func activeConstraint(greaterThanOrEqualTo anchor: NSLayoutAnchor, constant c: CGFloat) {
    return self.constraint(greaterThanOrEqualTo: anchor, constant: c).isActive = true
  }

  func activeConstraint(lessThanOrEqualTo anchor: NSLayoutAnchor) {
    return self.constraint(lessThanOrEqualTo: anchor).isActive = true
  }

  func activeConstraint(lessThanOrEqualTo anchor: NSLayoutAnchor, constant c: CGFloat) {
    return self.constraint(lessThanOrEqualTo: anchor, constant: c).isActive = true
  }
}

extension NSLayoutDimension {
  func activeConstraint(equalTo anchor: NSLayoutDimension, multiplier m: CGFloat) {
    return constraint(equalTo: anchor, multiplier: m).isActive = true
  }

  func activeConstraint(equalTo anchor: NSLayoutDimension, multiplier m: CGFloat, constant c: CGFloat) {
    return constraint(equalTo: anchor, multiplier: m, constant: c).isActive = true
  }

  func activeConstraint(equalToConstant c: CGFloat) {
    return constraint(equalToConstant: c).isActive = true
  }

  func activeConstraint(greaterThanOrEqualTo anchor: NSLayoutDimension, multiplier m: CGFloat) {
    return constraint(greaterThanOrEqualTo: anchor, multiplier: m).isActive = true
  }

  func activeConstraint(greaterThanOrEqualTo anchor: NSLayoutDimension, multiplier m: CGFloat, constant c: CGFloat) {
    return constraint(greaterThanOrEqualTo: anchor, multiplier: m, constant: c).isActive = true
  }

  func activeConstraint(greaterThanOrEqualToConstant c: CGFloat) {
    return constraint(greaterThanOrEqualToConstant: c).isActive = true
  }

  func activeConstraint(lessThanOrEqualTo anchor: NSLayoutDimension, multiplier m: CGFloat) {
    return constraint(lessThanOrEqualTo: anchor, multiplier: m).isActive = true
  }

  func activeConstraint(lessThanOrEqualTo anchor: NSLayoutDimension, multiplier m: CGFloat, constant c: CGFloat) {
    return constraint(lessThanOrEqualTo: anchor, multiplier: m, constant: c).isActive = true
  }

  func activeConstraint(lessThanOrEqualToConstant c: CGFloat) {
    return constraint(lessThanOrEqualToConstant: c).isActive = true
  }
}

Vamos implementar o datasource do collectionView. Adicione o código abaixo ao final do arquivo HomeViewController.swift.

// MARK: -
extension HomeViewController: UICollectionViewDataSource {
  // MARK: - UICollectionViewDataSource
  func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return tweets?.count ?? 0
  }

  func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: twitterCellId, for: indexPath) as! TwitterCell

    cell.tweet = tweets?[indexPath.item]

    return cell
  }
}

No método collectionView(_:numberOfItemsInSection:) retornamos a quantidade de itens da propriedade tweets.

No método collectionView(_:cellForItemAt:) obtemos um TwitterCell com o método dequeueReusableCell(withReuseIdentifier:for:) e atribuimos o tweet a célula.

No delegate iremos adicionar o método collectionView(_:layout:sizeForItemAt:) para retornar uma célula com a largura da tela e com 200 de altura.

// MARK: -
extension HomeViewController: UICollectionViewDelegateFlowLayout {
  // MARK: - UICollectionViewDelegateFlowLayout
  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    return CGSize(width: view.frame.width, height: 200)
  }
}

Criando a Interface

Agora vamos criar a célula que irá mostrar o tweet. Crie um novo arquivo TwitterCell.swift e adicione o código abaixo.

import UIKit

class TwitterCell: UICollectionViewCell {
  var tweet: Tweet?

  override init(frame: CGRect) {
    super.init(frame: frame)

    setupView()
  }

  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  private func setupView() {
    backgroundColor = .green
  }
}

Se rodarmos o programa agora iremos ver 50 células na cor verde ocupando todo o espaço horizontal da tela.

Screenshot 02

Agora vem a parte trabalhosa. Iremos criar todos os componentes que serão exibidos na tela. São eles profileImageView do tipo UIImageView, nameLabel, screenNameLabel e timeLabel do tipo UILabel, textTextView do tipo UITextView, replyButton, retweetButton e favoriteButton do tipo UIButton, replyCountLabel, retweetCountLabel e favoriteCountLabel do tipo UILabel, dividerLine do tipo UIView e alguns UIStackViews para organizar o layout.

No final teremos o mesmo que o layout a seguir.

Tweet Layout

let profileImageView: UIImageView = {
  let iv = UIImageView()
  iv.contentMode = .scaleAspectFill
  iv.layer.cornerRadius = 9
  iv.layer.masksToBounds = true
  iv.translatesAutoresizingMaskIntoConstraints = false

  return iv
}()

let nameLabel: UILabel = {
  let label = UILabel()
  label.font = UIFont.nameFont()
  label.textColor = UIColor.charcoalGrey
  label.translatesAutoresizingMaskIntoConstraints = false

  return label
}()

let screenNameLabel: UILabel = {
  let label = UILabel()
  label.font = UIFont.timeLabelFont()
  label.textColor = UIColor.blueyGrey
  label.translatesAutoresizingMaskIntoConstraints = false

  return label
}()

 let timeLabel: UILabel = {
  let label = UILabel()
  label.font = UIFont.timeLabelFont()
  label.textColor = UIColor.blueyGrey
  label.translatesAutoresizingMaskIntoConstraints = false

  return label
}()

let textTextView: UITextView = {
  let tv = UITextView()
  tv.font = UIFont.textFont()
  tv.textColor = UIColor.charcoalGrey
  tv.contentInset = UIEdgeInsets(top: -4, left: -4, bottom: -4, right: -4)
  tv.isEditable = false
  tv.isScrollEnabled = false
  tv.translatesAutoresizingMaskIntoConstraints = false
  tv.backgroundColor = .clear

  let paragraphStyle = NSMutableParagraphStyle()
  //paragraphStyle.lineSpacing = 16.5
  paragraphStyle.firstLineHeadIndent = 0
  paragraphStyle.headIndent = 0

  tv.linkTextAttributes = [NSFontAttributeName: UIFont.textFont(), NSParagraphStyleAttributeName: paragraphStyle, NSForegroundColorAttributeName: UIColor.windowsBlue]

  return tv
}()

let stackView: UIStackView = {
  let sv = UIStackView()
  sv.axis = .horizontal
  sv.distribution = .fillEqually
  sv.translatesAutoresizingMaskIntoConstraints = false

  return sv
}()

let replyButton: UIButton = {
  let button = UIButton(type: .system)
  button.setImage(#imageLiteral(resourceName: "Reply"), for: .normal)
  button.tintColor = UIColor.cloudyBlue
  button.translatesAutoresizingMaskIntoConstraints = false

  return button
}()

let replyCountLabel: UILabel = {
  let label = UILabel()
  label.font = UIFont.replyFont()
  label.textColor = UIColor.blueyGrey
  label.textAlignment = .left
  label.setContentHuggingPriority(1, for: .horizontal)
  label.translatesAutoresizingMaskIntoConstraints = false

  return label
}()

let replyStackView: UIStackView = {
  let sv = UIStackView()
  sv.axis = .horizontal
  sv.distribution = .fill
  sv.spacing = 3
  sv.translatesAutoresizingMaskIntoConstraints = false

  return sv
}()

let retweetButton: UIButton = {
  let button = UIButton(type: .system)
  button.setImage(#imageLiteral(resourceName: "Retweet"), for: .normal)
  button.tintColor = UIColor.cloudyBlue
  button.translatesAutoresizingMaskIntoConstraints = false

  return button
}()

let retweetCountLabel: UILabel = {
  let label = UILabel()
  label.font = UIFont.replyFont()
  label.textColor = UIColor.blueyGrey
  label.textAlignment = .left
  label.setContentHuggingPriority(1, for: .horizontal)
  label.translatesAutoresizingMaskIntoConstraints = false

  return label
}()

let retweetStackView: UIStackView = {
  let sv = UIStackView()
  sv.axis = .horizontal
  sv.distribution = .fill
  sv.spacing = 3
  sv.translatesAutoresizingMaskIntoConstraints = false

  return sv
}()

let favoriteButton: UIButton = {
  let button = UIButton(type: .system)
  button.setImage(#imageLiteral(resourceName: "Favorite"), for: .normal)
  button.tintColor = UIColor.cloudyBlue
  button.translatesAutoresizingMaskIntoConstraints = false

  return button
}()

let favoriteCountLabel: UILabel = {
  let label = UILabel()
  label.font = UIFont.replyFont()
  label.textColor = UIColor.blueyGrey
  label.textAlignment = .left
  label.setContentHuggingPriority(1, for: .horizontal)
  label.translatesAutoresizingMaskIntoConstraints = false

  return label
}()

let favoriteStackView: UIStackView = {
  let sv = UIStackView()
  sv.axis = .horizontal
  sv.distribution = .fill
  sv.spacing = 3
  sv.translatesAutoresizingMaskIntoConstraints = false

  return sv
}()

let emptyStackView: UIStackView = {
  let sv = UIStackView()
  sv.axis = .horizontal
  sv.distribution = .fill
  sv.translatesAutoresizingMaskIntoConstraints = false

  return sv
}()

let dividerLine: UIView = {
  let view = UIView()
  view.backgroundColor = UIColor.paleGrey
  view.translatesAutoresizingMaskIntoConstraints = false

  return view
}()

No método setupViews() iremos substituir o código backgroundColor = .green pelo código abaixo.

addSubview(profileImageView)
addSubview(nameLabel)
addSubview(screenNameLabel)
addSubview(timeLabel)
addSubview(textTextView)
addSubview(stackView)
addSubview(dividerLine)

replyStackView.addArrangedSubview(replyButton)
replyStackView.addArrangedSubview(replyCountLabel)
stackView.addArrangedSubview(replyStackView)

retweetStackView.addArrangedSubview(retweetButton)
retweetStackView.addArrangedSubview(retweetCountLabel)
stackView.addArrangedSubview(retweetStackView)

favoriteStackView.addArrangedSubview(favoriteButton)
favoriteStackView.addArrangedSubview(favoriteCountLabel)
stackView.addArrangedSubview(favoriteStackView)

stackView.addArrangedSubview(emptyStackView)

profileImageView.topAnchor.activeConstraint(equalTo: topAnchor, constant: 13)
profileImageView.leadingAnchor.activeConstraint(equalTo: leadingAnchor, constant: 12)
profileImageView.heightAnchor.activeConstraint(equalToConstant: 48)
profileImageView.widthAnchor.activeConstraint(equalToConstant: 48)

nameLabel.topAnchor.activeConstraint(equalTo: topAnchor, constant: 10)
nameLabel.leadingAnchor.activeConstraint(equalTo: profileImageView.trailingAnchor, constant: 9)

screenNameLabel.topAnchor.activeConstraint(equalTo: topAnchor, constant: 12)
screenNameLabel.leadingAnchor.activeConstraint(equalTo: nameLabel.trailingAnchor, constant: 4)

timeLabel.topAnchor.activeConstraint(equalTo: topAnchor, constant: 12)
timeLabel.leadingAnchor.activeConstraint(greaterThanOrEqualTo: screenNameLabel.trailingAnchor, constant: 4)
timeLabel.trailingAnchor.activeConstraint(equalTo: trailingAnchor, constant: -13)

textTextView.topAnchor.activeConstraint(equalTo: nameLabel.bottomAnchor, constant: 2)
textTextView.leadingAnchor.activeConstraint(equalTo: profileImageView.trailingAnchor, constant: 9)
textTextView.trailingAnchor.activeConstraint(equalTo: trailingAnchor, constant: -11)

stackView.topAnchor.activeConstraint(equalTo: textTextView.bottomAnchor, constant: 12)
stackView.leadingAnchor.activeConstraint(equalTo: profileImageView.trailingAnchor, constant: 9)
stackView.trailingAnchor.activeConstraint(equalTo: trailingAnchor, constant: -11)
stackView.heightAnchor.activeConstraint(equalToConstant: 17)

dividerLine.topAnchor.activeConstraint(greaterThanOrEqualTo: stackView.bottomAnchor, constant: 13)
dividerLine.leadingAnchor.activeConstraint(equalTo: leadingAnchor)
dividerLine.trailingAnchor.activeConstraint(equalTo: trailingAnchor)
dividerLine.bottomAnchor.activeConstraint(equalTo: bottomAnchor)
dividerLine.heightAnchor.activeConstraint(equalToConstant: 1.5)

Se rodarmos o aplicativo agora teremos apenas as imagens.

Screenshot 03

Vamos preencher as informações do tweet com o objeto Tweet passado pelo HomeViewController. A forma mais simples de fazer isto é com o didSet da propriedade tweet.

private var dataTask: URLSessionDataTask?

var tweet: Tweet? {
  didSet {
    if let tweet = tweet {
      getProfileImage()
      timeLabel.text = Date.getFormattedDiffTime(tweet.created_at)
      textTextView.text = tweet.text
      retweetCountLabel.text = (tweet.retweet_count > 0) ? "\(tweet.retweet_count)" : ""
      favoriteCountLabel.text = (tweet.favorite_count > 0) ? "\(tweet.favorite_count)" : ""

      if let user = tweet.user {
        nameLabel.text = user.name
        screenNameLabel.text = "@\(user.screen_name)"
      }
    }
  }
}

Chamamos o método getProfileImage() para obtermos a imagem do profile do usuário que postou o tweet.

O método estático Date.getFormattedDiffTime(_:) transforma um Date em uma string com a quantidade de tempo e a unidade. Exemplo 1d para um dia ou 21m para 21 minutos.

Os outros campos são preenchidos com as propriedades do tweet.

extension Date {
  static func getFormattedDiffTime(_ date: Date) -> String {
    let diffComponents = Calendar.autoupdatingCurrent.dateComponents([.second, .minute, .hour, .day, .month, .year], from: date, to: Date())

    if let years = diffComponents.year, years > 0 {
      return "\(years)y"
    }

    if let months = diffComponents.month, months > 0 {
      return "\(months)m"
    }

    if let days = diffComponents.day, days > 0 {
      return "\(days)d"
    }

    if let hours = diffComponents.hour, hours > 0 {
      return "\(hours)h"
    }

    if let minutes = diffComponents.minute, minutes > 0 {
      return "\(minutes)m"
    }

    if let seconds = diffComponents.second {
      return "\(seconds)s"
    }

    return ""
  }
}

Rodando o aplicativo temos as informações do tweet.

Screenshot 04

O nosso tweet está meio sem graça porque os links, mentions e hashtags estão sem destaques no texto do tweet. Vamos mudar isto utilizando NSAttributedString para destacar estes itens.

private func formattedTweet() -> NSAttributedString {
  if let tweet = tweet {
    let paragraphStyle = NSMutableParagraphStyle()
    //paragraphStyle.lineSpacing = 16.5
    paragraphStyle.firstLineHeadIndent = 0
    paragraphStyle.headIndent = 0

    let attributedString = NSMutableAttributedString(string: tweet.text, attributes: [NSFontAttributeName: UIFont.textFont(), NSParagraphStyleAttributeName: paragraphStyle, NSForegroundColorAttributeName: UIColor.charcoalGrey])

    if let urls = tweet.entities?.urls {
      for url in urls {
        let linkAttributedString = NSAttributedString(string: url.display_url, attributes: [NSLinkAttributeName: url.expanded_url])

        attributedString.replaceCharacters(in: (attributedString.string as NSString).range(of: url.url.absoluteString), with: linkAttributedString)
      }
    }

    if let hashtags = tweet.entities?.hashtags {
      for hashtag in hashtags {
        let hashtagAttributedString = NSAttributedString(string: "#\(hashtag.text)", attributes: [NSFontAttributeName: UIFont.textFont(), NSParagraphStyleAttributeName: paragraphStyle, NSForegroundColorAttributeName: UIColor.windowsBlue])

        attributedString.replaceCharacters(in: (attributedString.string as NSString).range(of: "#\(hashtag.text)"), with: hashtagAttributedString)
      }
    }

    if let mentions = tweet.entities?.mentions {
      for mention in mentions {
        let hashtagAttributedString = NSAttributedString(string: "@\(mention.screen_name)", attributes: [NSFontAttributeName: UIFont.textFont(), NSParagraphStyleAttributeName: paragraphStyle, NSForegroundColorAttributeName: UIColor.windowsBlue])

        attributedString.replaceCharacters(in: (attributedString.string as NSString).range(of: "@\(mention.screen_name)"), with: hashtagAttributedString)
      }
    }

    if let medias = tweet.entities?.medias {
      for media in medias {
        let linkAttributedString = NSAttributedString(string: media.display_url, attributes: [NSLinkAttributeName: media.expanded_url])

        attributedString.replaceCharacters(in: (attributedString.string as NSString).range(of: media.url.absoluteString), with: linkAttributedString)

        getImage(from: media.media_url_https) { image in
          if let thumbnailImage = image.getThumbnail(width: self.frame.width - 12 - 48 - 9 - 11 - 6) {
            let attributedText = NSMutableAttributedString(attributedString: self.textTextView.attributedText)

            let mediaAttachment = NSTextAttachment()
            mediaAttachment.image = thumbnailImage

            let mediaAttributedString = NSAttributedString(attachment: mediaAttachment)

            attributedText.append(NSAttributedString(string: "\n\n"))
            attributedText.append(mediaAttributedString)

            self.textTextView.attributedText = attributedText
          }
        }
      }
    }

    // Fix the link font
    attributedString.enumerateAttributes(in: NSRange.init(location: 0, length: attributedString.length), options: .reverse) { attributes, range, _ in
      if let _ = attributes[NSLinkAttributeName] {
        attributedString.removeAttribute(NSFontAttributeName, range: range)
        attributedString.addAttribute(NSFontAttributeName, value: UIFont.textFont(), range: range)
      }
    }

    return attributedString
  }

  return  NSAttributedString(string: "")
}

O código acima é meio extenso mas é bem simples. Primeiro formatamos o texto do tweet de acordo com o design. Alterando a cor a fonte e o parágrafo.

Em seguida criamos links para os endereços dos urls. O Twitter resume um endereço web e portanto temos que utilizar valores diferentes pro links exibidos e pros endereços enviados para o browser.

Depois é a vez dos mentions e hashtags que são destacados com a cor azul.

Outro item que vamos alterar são as imagens que serão exibidas no tweet. Utilizamos um NSTextAttachment para inseri-las junto ao texto. Ao obtermos a imagem do endereço do link usamos o método getThumbnail(width:) do UIImage para redimensionar a imagem. A largura da imagem deverá ser a largura do textView menos uma margem e pra chegarmos no valor precisamos de um pouco de matemática.

Temos a largura da célula menos o leading margin, width e trailing margin do profileImageView. Em seguida subtraimos o trailing margin do textTextView, menos 6 pixels para sobrar uma margem entre a imagem e o textView. Também inseri duas quebras de linha para termos um espaço entre o final do texto e a imagem.

Por último, passo por todo o attributedText procurando o atributo NSLinkAttributeName para alterar as fontes dos links. Não sei porque não consegui alterá-las via propriedade linkTextAttributes do UITextView.

extension UIImage {
  func getThumbnail(width: CGFloat) -> UIImage? {
    let size = self.size

    let aspectRatio = size.width / size.height

    // Figure out what our orientation is, and use that to form the rectangle
    var newSize: CGSize

    if(size.width > size.height) {
      newSize = CGSize(width: width, height: width / aspectRatio)
    } else {
      newSize = CGSize(width: width / aspectRatio,  height: width)
    }

    // This is the rect that we've calculated out and this is what is actually used below
    let rect = CGRect(x: 0, y: 0, width: newSize.width, height: newSize.height)

    // Actually do the resizing to the rect using the ImageContext stuff
    UIGraphicsBeginImageContextWithOptions(newSize, false, 1.0)
    self.draw(in: rect)
    let newImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()

    return newImage
  }
}

pra finalizar vamos trocar o código textTextView.text = tweet.text por textTextView.attributedText = formattedTweet().

Rodando o aplicativo novamente temos as formatações das urls, mentions e hashtags mas não vemos as imagens. Isto acontece porque a célula não tem o tamanho necessário para exibir as imagens.

Screenshot 05

Para calcular o tamanho da célula usaremos o método getThumbnail(size:) para obtermoso tamanho das imagens. Voltando ao arquivo HomeViewController.swift, adicione o seguinte código ao início do método collectionView(_:layout:sizeForItemAt:).

if let tweet = tweets?[indexPath.item] {
  let textViewWidth: CGFloat = view.frame.width - 12 - 48 - 9 - 11
  let height = tweet.text.heightWithConstrained(width: textViewWidth, font: UIFont.textFont()) + 20
  let mediaSize: CGFloat = (tweet.entities?.medias.map({ self.getThumbnail(size: $0.mediaSize).height + 25 }).reduce(0, +))!

  return CGSize(width: view.frame.width, height: 10 + 17 + 2 + height + mediaSize + 12 + 17 + 13 + 1.5)
}

Primeiro calculamos o tamanho do textView igual fizemos anteriormente. Depois chamamos o método heightWithConstrained(width:font:) a partir do texto do tweet. Este método serve para calcular a altura de determinado texto de acordo com a fonte e a largura do componente. O método não é muito preciso e adicionei 20 ao valor retornado pra compensar as margens internas do textView.

Na sequencia utilizamos o método getThumbnail(size:) para calcular a altura da imagem. Ele recebe a largura da imagem retornada pela API e retorna o tamanho da imagem redimensionada para caber no textView. O método map percorre o array das imagens e retorna um array com a altura delas mais 25 para compensar a quebra de linha. O método reduce soma todos os itens do map calculando a altura total.

No final retornarmos a altura de todos os componentes da célula mais os espaços entre eles.

O método getThumbnail(size:) possui o seguinte código.

private func getThumbnail(size: CGSize) -> CGSize {
  let aspectRatio = size.width / size.height
  let width: CGFloat = view.frame.width - 12 - 48 - 9 - 11 - 6

  if size.width > size.height {
    return CGSize(width: width, height: width / aspectRatio)
  }

  return CGSize(width: width / aspectRatio, height: width)
}
extension String {
  func heightWithConstrained(width: CGFloat, font: UIFont) -> CGFloat {
    let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
    let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)

    return boundingBox.height
  }
}

Ainda dentro da extension do UICollectionViewDelegateFlowLayout vamos adicionar o método collectionView(_:layout:minimumLineSpacingForSectionAt:) para retirar os espaços entre as células.

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
  return 0
}

Rodando o aplicativo temos os tweets com os links e as imagens.

Screenshot 06

Para finalizarmos o projeto vamos criar os itens da navigationBar. No fonte HomeViewController.swift adicione o código a seguir abaixo do método viewDidLoad().

private func makeNavigationBar() {
  let homeLabel: UILabel = {
    let label = UILabel()
    label.font = UIFont.titleMenubarFont()
    label.textColor = .white
    label.text = "HOME"
    label.translatesAutoresizingMaskIntoConstraints = false

    label.sizeToFit()

    return label
  }()

  let addContactBarButtonItem: UIBarButtonItem = {
    let button = UIBarButtonItem(image: #imageLiteral(resourceName: "Add Contact"), style: .plain, target: self, action: nil)

    return button
  }()

  let searchBarButtonItem: UIBarButtonItem = {
    let button = UIBarButtonItem(image: #imageLiteral(resourceName: "Search"), style: .plain, target: self, action: nil)

    return button
  }()

  let composeTweetBarButtonItem: UIBarButtonItem = {
    let button = UIBarButtonItem(image: #imageLiteral(resourceName: "Compose Tweet"), style: .plain, target: self, action: nil)

    return button
  }()

  navigationItem.titleView = homeLabel
  navigationItem.leftBarButtonItem = addContactBarButtonItem
  navigationItem.rightBarButtonItems = [composeTweetBarButtonItem, searchBarButtonItem]
}

O método makeNavigationBar() é bem simples. Criamos um UILabel com a fonte e a cor do design e atribuímos à propriedade navigationItem.titleView. Depois criamos os três botões da navigationBar e os atribuimos às propriedades navigationItem.leftBarButtonItem e navigationItem.rightBarButtonItems.

Pra finalizar precisamos adicionar a chamada ao método makeNavigationBar() dentro do método viewDidLoad().

Conclusão

A versão final ficou assim:

Screenshot 07

O Zeplin facilita bastante o trabalho de visualizar o layout criado no Sketch. As funcionalidades de exportação de código de cor e fonte são bem práticas. A ferramenta permite colocar comentários sobre os itens do projeto para poder tirar dúvidas com o designer e poupa o custo de ter que comprar licenças para as maquinas dos desenvolvedores.

Outro ponto que vimos neste artigo foi a criação da interface via código. As modificações do autolayout no iOS 9 simplificaram a construção das constraints o que deixou o código mais fácil de ler e escrever.

Não desenvolvemos todas as propriedades do tweet que ainda possui extended_entities com gifs animados, vídeos e coleções de fotos. Outra coisa que vocês podem explorar e o retweet e reply que também não abordamos por completo aqui no artigo.

Espero que tenham gostado do artigo. Qualquer dúvida, crítica ou sugestão de novos assuntos deixe um comentário abaixo. Caso queira entrar em contato pode me chamar no twitter @mateusfsilva. Se gostou compartilhe.

Publicidade

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s