Desenvolvendo pacotes em Swift

Desenvolvendo pacotes em Swift

Quando estamos desenvolvendo um projeto, é importante dividí-lo em pequenos pedaços. Isso é muito relevante para, dentre outras coisas, facilitar trabalho em time e desenvolvimento do projeto. Por exemplo, o desenvolvimento em plataformas como iOS e tvOS é sobretudo UI, então uma boa forma de se trabalhar é dividir os componentes de UI em pacotes distintos.
Desse modo, teremos como vantagem a reutilização deles em outros projetos, poderem ser divulgados separadamente, assim como a redução no tempo de build e código no projeto original. Apesar de desenvolver um pacote levar mais tempo, por precisar gerar um novo projeto e fazer o desacoplamento, a médio e curto prazo é muito vantajoso.

Simplificadamente, podemos dizer que quanto mais pacotes, menos acoplamento.
Quando estiver desenvolvendo um pacote, lembre-se que ele deve se focar em resolver apenas um problema. Você pode ver mais sobre vatangens e boas práticas no desenvolvimento de pacotes nessa talk do jspahrsummers, um dos colaboradores do Carthage.

Essa postagem aborda uma visão geral sobre desenvolvimento de pacotes em Swift e explica passo a passo de como gerar um novo pacote.

Gerenciadores de dependências

Há três gerenciadores de dependências principais para Swift: CocoaPods, Carthage, e Swift Packet Manager (SPM).

O CocoaPods é um gerenciador de dependência centralizado por default. Ele é popular e fácil de se utilizar. Ao especificar suas dependências em um único arquivo, o Podfile, o CocoaPods se responsabiliza em automaticamente resolve as dependências recursivamente, recolhe o código fonte de todas, para então criar/atualizar automaticamente o workspace do Xcode, para o seu projeto estar pronto para ser compilado.
Um inconveniente do CocoaPods é que, quando um pacote for atualizado, é necessário re-compilar todos os demais pacotes também.

Outro gerenciador de dependência é Carthage. Ele foi desenvolvido com uma filosofia diferente do CocoaPods. O Carthage, em contraste ao CocoaPods, é totalmente descentralizado e visa ser uma ferramenta simples e pouco invasiva. Por conta disso, parte do trabalho de integração das dependências é deixado para o usuário fazer. Desse modo, ele terá maior controle sobre o seu projeto.

Por fim, temos o Swift Packet Manager (SPM). Ele é o gerenciador de dependências cross-platform oficial do Swift. É simples de se utilizar como o CocoaPods, da qual você escreve as dependências num único arquivo (Package.swift) e o SPM é responsável por recolher os pacotes e atualizar o projeto no Xcode. Ele tem suporte apenas para projetos em Swift, logo, se o seu projeto tiver Obj-C, não é uma boa escolha usá-lo.
Além disso, ele é popular para projetos Swift server side.

Além de usar gerenciadores, uma outra possibilidade é adicionar dependências manualmente. Foge do escopo explanar isso nessa postagem, mas você pode ler aqui sobre isso.

Geradores

Quando for desenvolver um novo pacote Swift, é necessário montar um projeto para isso; deve-se, por exemplo, configurar o Xcode, criar o .podspec para suportar o CocoaPods, o Package.swift para o SPM, talvez fazer um projeto de exemplo... Como essas são atividades recorrentes e repetitivas, diferentes geradores foram desenvolvidos para automatizar esse processo.

Aqui vamos descrever brevemente dois populares geradores.

Template do CocoaPods

Um dos comandos do CocoaPods é pod lib create. Ele gera um esqueleto de um pod, já fazendo a organização do projeto no Xcode, além de recomendar boas práticas, como a criação de um exemplo e testes.
Para usá-lo, basta inserir o comando e ir preenchendo as informações pedidas. Você pode ver mais sobre ele na documentação do CocoaPods.

Apesar do template do CocoaPods ser muito fácil de usar e prático por já vim embutido ao instalar o CocoaPods, ele carace de flexibilidade. O template gerado é para iOS, portanto, se quiser usá-lo para desenvolver um pacote cross-platform, ou somente para tvOS, por exemplo, terá um ernorme trabalho em modificar o esboço gerado.
Além disso, o template gerado não suporta automaticamente o Carthage e SPM. Portanto, se o seu pacote precisa suporta esses gerenciadores também, precisará configurar isso manualmente.

SwiftPlate

Diferente do template do CocoaPods, o SwiftPlate é uma ferramenta para gerar uma biblioteca Swift cross-plataform. Além disso, também tem como vantagem que o projeto gerado já estará disponível para diferentes gerenciadores: CocoaPods, Carthage e SPM.

Ele também é muito fácil de se usar, bastando preencher algumas informações que vão sendo pedidas. Porém, também carece de algumas flexibilidades, como poder escolher as scheme do xcode project a serem geradas.
Algumas features do template do CocoaPods e que não tem no SwiftPlate é gerar um projeto de exemplo automaticamente e a possibilidade de gerar um projeto de testes automaticamente, podendo configurá-lo com pacotes como Nimb e Quick.

Montando projeto de pacote do zero

Como vimos anteriormente, os geradores são práticos e fáceis de se usar, porém, como desvantagem, carecem de flexibilidade. Talvez você queira escrever um pacote só para tvOS, por exemplo, e com os geradores citados isso não poderia ser feito totalmente de forma automática.

Desse modo, nos passo a passo a seguir vamos montar do zero um projeto de um pacote para tvOS, chamado "MyAmazingPod". Em algumas situações pode ser que você precise fazer exatamente isso, como caso esteja escrevendo um componente de UI específico para tvOS. Eu já tive algumas situações assim.
Além disso, é útil aprender a montar o ambiente do zero, por ajudar a criar uma visão melhor de como configurar o Xcode - algo que pode ser muito útil em outras situações.

Criando projetos no Xcode

Antes de tudo, crie uma pasta com o nome do seu pacote. No caso, "MyAmazingPod". Tenha cuidado ao nomear o seu pacote, pois nomes grandes de mais (~24 caracteres) podem causar erros.

É recomendável que o seu pacote tenha algum exemplo. Por exemplo, se o seu pacote for um componente de UI, então é bom ter um projeto demonstrando o uso do seu componente. Já se o pacote for alguma biblioteca, então um playground demonstrando-o é bom.
Visando isso, a partir da pasta original criaremos dois projetos no Xcode:

Se estivéssemos criando um pacote para mais de uma plataforma, como tvOS e iOS, e houvessem diferenças importantes entre elas, poderíamos criar duas pastas de exemplo, "Example-tvOS" e "Example-iOS". As configurações que seguem seriam parecidas também para esse outro caso.

Agora que temos dois projetos no Xcode, precisamos criar um workspace. Um workspace é, basicamente, a junção de diferentes projetos do Xcode em um só ambiente. Além de ser prática para programar, é importante por podermos criar dependências entre os projetos.
Para isso, feche os projetos no Xcode que estiverem aberto (para evitar um antigo bug) e crie um workspace (ˆ⌘N) chamado "MyAmazingPod". Então, apenas clique e arraste o ícone no Finder dos outros projetos no project navigation do workspace.

No projeto "MyAmazingPod", você verá que o próprio Xcode criou dois arquivos automaticamente: um chamado "MyAmazingPod.h" e outro chamado "Info.plist". Mantenham eles, pois são úteis para o sistema saber como lidar operar com o .framework.

Até agora, você deve ter obtido algo mais ou menos assim. No link ao lado você pode ver como deve ficar o projeto até o momento.

Configurando para CocoaPods

Primeiramente, vamos configurar o nosso pacote para ser compatível com o CocoaPods. Outros benefícios ao fazer isso é que poderemos usar outros pods em nosso pod, e também vamos estar configurando o nosso projeto de exemplo para usar o pacote que estamos criando.

Para ser compatível com o CocoaPods, precisamos criar um podspec. Um "podspec" é um arquivo de metainformação sobre o pacote, para o CocoaPods saber, dentre outras informações, o nome do pacote, o autor, descrição, dependências...
Desse modo, crie um arquivo na pasta raiz chamado MyAmazingPod.podspec. Nele, insira o seguinte conteúdo:

Pod::Spec.new do |s|
  s.name         = "MyAmazingPod"
  s.version      = "0.0.1"
  s.summary      = "A brief summary for your amazing pod"
  s.homepage     = "www.macalogs.com.br"
  s.license      = { :type => "MIT", :file => "LICENSE" }
  s.author       = { "Bruno Macabeus" => "my@email.com" }

  s.tvos.deployment_target = "10.0"

  s.source       = { :git => "https://github.com/macabeus/MyAmazingPod.git", :tag => s.version }
  s.source_files = "MyAmazingPod/MyAmazingPod/*.swift", "MyAmazingPod/MyAmazingPod/*.xib"

  s.dependency 'Cartography', '~> 1.1'
end

Atenção: Nesse exemplo, estamos considerando que o nosso pod depende de outro pod, no caso, do Cartography. Você pode ler detalhadamente sobre cada parâmetro do .podspec na documentação oficial do CocoaPods.
Ah, uma breve dica: arraste o arquivo do podspec para o projeto no Xcode, para criar uma referência a ele. Isso facilita quando for mexer nele no futuro.

Além disso, precisamos configurar o Example para usar o pod que estamos criando e, além disso, o MyAmazaingPod precisar baixar as dependências dele (no caso, o Cartography). Para isso, crie na pasta raiz um arquivo chamado Podfile com o seguinte conteúdo:

platform :tvos, '10.2'

# Ao invés do CocoaPods criar um novo workspace,
# ele deve usar o já criado, e trabalhar nele
workspace './MyAmazingPod.xcworkspace'

# O nosso workspace tem dois target, um que é do Example,
# e outro que do MyAmazingPod.
# Cada target tem dependências diferentes, e vamos definí-las a seguir

target 'MyAmazingPod' do
  project 'MyAmazingPod/MyAmazingPod.xcodeproj'

  use_frameworks!

  # Tal como definimos no podspec,
  # o nosso pod precisa do Cartography, e vamos baixá-lo aqui 
  pod 'Cartography', '~> 1.1'
end

target 'Example' do
  project 'Example/Example.xcodeproj'

  use_frameworks!

  # Definir para o nosso projeto de exemplo usar o nosso pod
  pod 'MyAmazingPod', :path => '.'
end

É recomendável que tenhamos um arquivo de licença. Para fins de exemplo, vamos usar a licensa MIT. Então, execute o seguinte comando:

curl -d'{ "copyright": "Macalogs" }' https://rem.mit-license.org > LICENSE`

Após criar o MyAmazingPod.podspec, o Podfile e o LICENSE, feche o Xcode e execute o seguinte comando no terminal: pod install.

Estamos quase terminando a configuração. Abra novamente o workspace e selecione a scheme "Example". Vá em "Edit Scheme", aba "Build" e clique no "+". Então adicione "MyAmazingPod" como dependência. Além disso, marque o checkbox "Shared" para que essa sua configuração seja compartilhada, por exemplo, no git. Veja o passo a passo na foto logo abaixo.
Isso que fizemos é útil para, quando algo for modificado no projeto do MyAmazingPod, possamos executar diretamente o Example, sem precisar compilar separadamente o outro projeto.
Nesse momento, compile o Example. Isso serve para o Xcode já gerar o module do MyAmazingPod, evitando problemas em alegar que o module não existe durante o processo de compilação em pararelo dos projetos.

Agora, vamos começar a usar o nosso pod! Vamos criar uma struct no pod e usá-la no projeto de exemplo. Adicione um arquivo chamado Amazing.swift dentro da pasta "MyAmazingPod", com o seguinte:

import Foundation

public struct Amazing {
    let phrase: String

    public init(phrase: String) {
        self.phrase = phrase
    }
}

Aqui há um incoveniente em relação ao funcionamento do CocoaPods: tal como explicado pelo Orta, ele não checa mudanças estruturais (como arquivo novo/removido). Ou seja, enquanto você estiver desenvolvendo e adicionar/remover um arquivo de seu pacote, quando for testar no projeto de exemplo precisará executar o comando pod update MyAmazingPod. Desse modo, execute esse comando para continuarmos.

Agora, deixe o ViewController.swift assim:

import UIKit
import MyAmazingPod

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let x = Amazing(phrase: "foo")
        print(x)
    }

}

Por fim, execute o projeto de exemplo e veja funcionar.

Até agora, você deve ter obtido algo mais ou menos assim.

Carthage

TODO: Escrever passo a passo de como portar para o Carthage

SPM

TODO: Escrever passo a passo de como portar para o SPM

Boas práticas para melhorar o seu pacote

A seguir, vamos falar brevemente algumas dicas e recomendações de leitura para saber como tornar o seu pacote melhor.

Dependência

Tal como demonstrado, você pode criar dependências em seu pacote. Ou seja, o seu pacote A pode usar algo do pacote B. Porém, tome cuidado ao fazer isso.

É óbvio que em algumas situações isso é necessário. Por exemplo, se estiver fazendo um wrapper para o Firebase, será necessário importar o Firebase em seu pacote. Outro exemplo é se estiver fazendo um pacote que tenha uma UI complexa, nesse caso talvez seja conveniente usar um outro pacote que facilite o layout da UI. Ele poderá ajudar na legibilidade e manutenção do pacote.
Porém, em outras situações talvez isso não seja tão necessário. Se a UI não for tão complexa assim, talvez seja melhor escrever um pouco mais, usando código vanilla. Usar pacotes externos em seu pacote aumentará mais ainda o tamanho dele e criará dependendências externas.
Basicamente, é necessário os mesmos cuidados que se deve tomar ao se adicionar um pacote em sua aplicação, com a diferença que a dependências em seu pacote será propagada para todos os projetos que o usam.

Nessa parte da documentação do SPM é explicado sobre "Dependency Hell".

Testes e cobertura de código

TODO

Documentando seu pacote

TODO

https://github.com/realm/jazzy

https://github.com/noffle/art-of-readme https://github.com/matiassingers/awesome-readme#articles

Styleguide

TODO

https://github.com/realm/SwiftLint

Conclusão

TODO