quarta-feira, 5 de janeiro de 2011

Criando apps para iPhone com Swift - Navegação



Navegação! Como navegar de uma cena para outra? Como criar apps que apresentam várias cenas ao usuário?

Muito bem. Agora, que sabemos um pouco sobre Swift e sobre Persistência de dados, é o momento de pensarmos mais um pouco em navegação. Temos vários tipos de navegação que podemos implementar em nossa app, e já vimos uma delas, que é baseada em Tab Bar (programa Conversor). Porém, o iOS permite vários tipos de navegação, e eu gostaria de mostrar mais algumas opções.



Eu não pretendo apresentar todos os tipos de navegação possíveis e nem considero isso prático. Vou mostrar casos de navegação bem simples, deixando os mais complexos para que você mesmo descubra.



Navegação entre cenas




Em uma Storyboard, podemos ter mais de uma scene ou cena. Crie um novo projeto no Xcode e escolha o template “Single View”.



Se você observar o Project Navigator, verá que há um arquivo de “Storyboard” e um único ViewController. Vamos adicionar uma nova cena, que é composta por:
  • Um Elemento Visual View Controller, com sua View;
  • Uma nova classe, derivada de UIViewController, para controlar a View;
  • Uma “Segue”, para permitir a navegação de uma cena para a outra.



O projeto completo está em “capt6/SimpleNavigation.zip”.



Adicionando uma nova cena no Storyboard




Selecione o arquivo “Main.storyboard” e, na “Object library”, selecione um Objeto “View Controller”, arrastando-o para o editor.




Você verá uma nova cena adicionada ao Storyboard. Poderá ficar “bagunçada”, uma sobre a outra. Você pode ajustar diminuindo o “zoom” e ajeitando as duas cenas, para ficarem uma ao lado da outra. Para aumentar ou diminuir o “zoom” do Storyboard, use um movimento de pinça no trackpad do seu Mac ou então use o menu “Editor / Canvas / Zoom”.



Bem, com uma cena ao lado da outra, agora vamos criar um “Label” em cada uma, contendo: “Cena 1”, para a primeira cena, e “Cena 2”, para a segunda cena. Na cena 1, crie um button com o texto “Navegar”.



Lembre-se de adicionar os “Constraints” para que o layout de auto ajuste. Para isto, selecione a primeira cena (a cena inteira), e, use o menu “Editor / Resolve auto layout issues / Add missing constraints”. Faça isso com a segunda cena também.



Adicionando uma classe derivada de UIViewController




Nada lhe impedirá de usar a mesma classe derivada de ViewController, para controlar ambas as cenas, embora essa não seja a prática recomendada. Então, vamos adicionar uma nova classe ao nosso projeto. Selecione a pasta do Projeto e use o menu “File / New / File...”. Na janela de template, escolha “iOS / Source” e “Cocoa Touch Class”:





Na próxima página, informe o nome da nova classe, que também será o nome do arquivo Swift, como: “ViewController2”, e certifique-se que “UIViewController” esteja selecionado na lista “Subclass of”.




Pronto! Uma nova classe será criada no nosso projeto, dentro do seu próprio arquivo Swift.



Agora, precisamos fazer com que a nova Cena (cena 2) use esse ViewController2. Para isto, selecione o “Main.storyboard” novamente, e, no Identity Inspector, mude a classe de “UIViewController” para “ViewController2”:





Ok. Agora, a cena 2 enviará seus eventos para a classe “ViewController2”.



Criando uma “Segue” entre as Cenas




Vamos criar uma “Segue” entre as duas cenas, muito parecida com as “Segues” que fizemos quando estudamos o TabBar. Pressione a tecla “control” e arraste uma linha, indo do botão “Navegar”, na cena 1, até a cena 2, e solte a tecla “control”. Você verá um menu de escolhas:




Temos vários tipos de “segue” ou ligação entre duas cenas:
  1. show”: Se a app estiver usando o modelo “Master / Detail”, pode exibir a tela na área de Master ou de Detail, dependendo do que estiver sendo exibido na tela;
  2. show detail”: Se a app estiver usando o modelo “Master / Detail”, exibe a nova cena na área de detalhes;
  3. present modally”: Mostra a nova cena de maneira modal sobre a anterior;
  4. present as popover”: Mostra a nova cena, ligada à anterior;
  5. custom”: Você controla a interação.



Para este caso, escolha “custom” e pronto! Agora, selecione a nova “segue” (a seta entre as duas cenas) e selecione o Attributes inspector, para dar um identificador a esta navegação:





Testando




Rode a app e você verá a cena 1. Ao clicar no botão “Navegar”, a cena 2 será carregada. Pronto!



Antes de navegar para outra cena, pode haver algumas coisas que desejemos fazer, por exemplo: Salvar alterações pendentes ou fechar conexões etc.



A classe UIViewController tem um método para isso: “prepareForSegue: sender:”, o qual podemos sobrescrever. No primeiro ViewController “ViewController.swift”, vamos interceptar o momento em que a “segue” será navegada, testando o seu identificador:



override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if segue.identifier == "paraCena2" {
       // fazer qualquer preparativo
       NSLog("Vai para a cena 2")
    }
}

Eis a imagem:





Como pode ver, a mensagem que enviamos usando o NSLog, aparece na Janela de depuração.






Depuração




Aliás, este é um bom momento para vermos como depurar o código, não? A primeira instrução de depuração que usamos foi o NSLog. Essa função gera mensagens que aparecerão na Janela de depuração do Xcode. Podemos até formatar Strings:



var xpto = 105
NSLog("XPTO vale \(xpto)")
var texto = "TESTE"
NSLog("O Texto é %@", texto)

Eis as saídas:



2015-04-01 08:44:50.281 SimpleNavigation[1786:68333] XPTO vale 105
2015-04-01 08:44:50.298 SimpleNavigation[1786:68333] O Texto é TESTE




A outra ferramenta de depuração é colocar “breakpoints” no código. Para isto, abra o nosso “ViewController.swift”. Dê um clique sobre o número da linha 26, onde está o comando “NSLog”, dentro do método “prepareForSegue: sender:”. Uma seta azul aparecerá, indicando que neste ponto foi inserido um “breakpoint”. Agora, rode o programa novamente, e clique no botão “Navegar”.




Agora, você está em modo de depuração, e a execução está parada na linha 26. No painel de baixo, você pode ver as variáveis que estão no contexto, expandindo suas propriedades. O painel ao lado, é onde aparecem as mensagens.



Se quiser, pode fazer: Continue, Step into, Step over e Step out, como em qualquer outra IDE moderna. Os comandos estão na parte de baixo do editor:





  • Desabilitar o “breakpoint”: Este botão desabilita todos os “breakpoints”, tornando-os translúcidos no código. Assim, a execução não vai mais parar neles;
  • Continue: Continua a execução a partir do “breakpoint”;
  • “Step over”: Executa o comando, sem entrar em seu método;
  • “Step into”: Executa o comando, entrando em seu método para depuração;
  • “Step out”: Se você fez um “Step into” e se arrependeu, este comando executa o resto do método e volta para o próximo comando, depois do que o invocou, entrando em “breakpoint”.



Usando um Navigation Controller




Há casos em que a navegação da nossa app é previsível, e que podemos usar uma “barra de navegação” para controlá-la, semelhante ao TabBar.



Este projeto está em “capt6/Navigation.zip”.



Vamos ver como é a navegação usando um Navigation Controller.



Para isto, crie um novo projeto no Xcode, selecionando o template “Single View”.



Adicionando um Navigation Controller




Abra o “Main.storyboard” e selecione o View Controller. Você pode fazer isso clicando na área livre da barra superior dele. Todo o entorno deve ficar em azul.





Agora, selecione o menu: “Editor / Embed in / Navigation Controller”. Você notará que foi adicionado um elemento Navigation Controller ao Storyboard, e que nossa cena original ganhou uma barra superior:




Ótimo! Vamos adicionar um Label na nossa view original, com o texto “Primeira View” (depois aplique “Editor / Resolve Auto layout issues / Add missing constraints”). Rode a app.



Você vai ver uma tela em branco, com uma barra cinza na parte superior:
Ok. Agora, vamos adicionar uma segunda view. Adicione outro ViewController ao Storyboard, como fizemos no exemplo do início do capítulo. Lembre-se de criar uma classe derivada de UIViewController para ele (e mude sua classe para a classe que acabamos de criar).



Agora, vamos adicionar um botão à barra de navegação, que nos permita navegar para o segundo ViewController. Não é um botão comum! Temos que selecionar um “Bar button item” e posicioná-lo na barra de navegação da primeira cena (não é na cena do Navigation Controller!).



Mude o nome do botão para “Segunda cena”. Agora, segure a tecla “control” e arraste uma linha a partir do botão “Segunda cena” até o segundo View Controller. Ao soltar a tecla “control”, selecione “show” no menu “action segue”.



Você verá que uma barra foi adicionada à nossa segunda cena. Adicione um label a ela, com o texto “cena 2”. Agora, rode a app.

Como pode ver, a partir do botão “Segunda cena”, podemos navegar para a cena 2, e que foi adicionado um botão “Back” automaticamente.



Como traduzir o botão “Back”?



Ainda vamos falar sobre I18N (Internacionalização) e L10N (localização), mas é possível traduzir o bar button muito facilmente:
  1. No seu projeto, abra a pasta “Supporting files”;
  2. Edite o arquivo “info.plist”;
  3. Expanda o item “Localizations”;
  4. Clique no pequeno botão “+”e acrescente “Portuguese”;
  5. Salve o arquivo.





Agora, mude o idioma do iOS Simulator:



  1. Se a app estiver rodando, pare-a;
  2. No iOS Simulator, volte para a primeira página (trackpad, segure um dedo e arraste o outro para a esquerda);
  3. Abra “Settings”;
  4. Abra “General” e depois “Language and region”;
  5. Mude “iPhone language” para Portuguese - Brasil e mude a Region para Brasil;



Agora, ao rodar a app, você verá que o botão “Back” foi traduzido para “Voltar”:





Interceptando a Segue



Selecione a Segue, abra o Attributes inspector e mude o identifier para “navegarCena2”. Agora, crie o evento “prepareForSegue: sender” no primeiro ViewController, da mesma maneira que fizemos no exemplo anterior:



override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
   if segue.identifier == "navegarCena2" {
     println("Vai para cena 2")
   }
}



A função “println” pode ser usada ao invés de NSLog.



Ok. E como eu intercepto o “Voltar”?



Quando usamos o “Back” button, não temos como interceptar a segue de retorno. Porém, podemos fazer um pequeno “truque” que funciona. No ViewController da segunda cena, acrescente este método:



override func willMoveToParentViewController(parent: UIViewController?) {
   if parent == nil {
     println("Vai voltar")
   }



Ele invoca este método 2 vezes, uma no momento em que carrega a segunda cena, e outra no momento em que o “Back” é pressionado. Se o parâmetro “parent” for nil, então é porque você está voltando.



Criando uma relação lista / detalhes




Muitas apps que você vai criar possuem uma relação lista / detalhes, também conhecida como: Master / Detail. Vamos ver como criar isso no iOS.



Complexidade




Criar uma app “Master / Detail” é algo bastante complexo no iOS, seja com Swift ou Objective C. Em defesa do iOS, eu diria que também não é muito simples fazer em Android.



Uma app Master / Detail é composta por uma cena que é uma lista de objetos, por exemplo Contatos, e, ao selecionar um deles, vamos para uma cena que contém os detalhes do Contato selecionado.



É claro que podemos fazer isso com o que já sabemos até agora. Podemos usar Navigation Controllers para fazer isso.



Só que o iOS nos provê elementos suficientes para automatizar esse processo de desenvolvimento, o que nos ajuda bastante.



Vamos ve o que já temos na parte visual. Vá até o Object Library e role um pouco a lista. Você verá um objeto Table View Controller:


Este tipo de View Controller é controlado por uma classe Swift, derivada de UITableViewController (que criaríamos em nosso projeto). Ele controla uma Table View, que é uma lista de elementos. Ele é o Delegate da Table View (recebe os eventos) e também sua Data Source (alimenta os dados da Table View).



Então, poderíamos usar um Table View Controller para a cena “Mestre”, e um outro View Controller para a cena “Detalhe”.



Essa solução funcionaria bem para um iPhone. Porém, e se o usuário tivesse um iPad? E se tivesse um iPhone 6 Plus, usando na horizontal? Teríamos muito espaço na tela, porém a navegação Mestre / Detalhe ainda seria substituindo a tela. Compo podemos fazer algo como isso:
Neste momento, temos um novo “ator” em cena: O Split View Controller!



Ele gerencia dois View Controllers: Um direito e um esquerdo! No iPhone, as cenas são apresentadas uma após a outra, mas, no iPad ou no iPhone 6 Plus (segurado na horizontal), as cenas são apresentadas lado a lado.



Então, precisaríamos usar:
  1. Split View Controller;
  2. Table View Controller para o Mestre;
  3. View Controller para o Detalhe;
  4. Um Managed Object Context para ambos.



Como estaríamos editando objetos persistentes, precisaríamos criar um Managed Object Context, certo? E, para ser acessado por ambos os View Controllers, ele precisaria ficar em um local acessível por ambos, por exemplo: O AppDelegate.



Template para projeto Master / Detail




Felizmente, o Xcode já tem um template para app Mestre / Detalhe embutido. Ao criar um novo projeto, você pode selecioná-lo:




Vamos lá! Crie uma app Mestre / Detalhe e experimente rodar no iPhone 6, iPhone 6 Plus e iPad! E mude a orientação, de vertical para horizontal.



Como fazer isso?



Bem, crie a app. Não precisa fazer mais nada para os dados aparecerem. É só clicar no botão “+” para adicionar mais um registro.



Para ir para o detalhe, basta tocar (clicar) em um registro. Se estiver usando iPhone 6 (ou iPhone 6 Plus na vertical), você verá um Botão “<Master” para voltar à cena Mestre.



Para deletar elementos da lista, vá a cena Mestre e toque em “Edit”. Uma lista de sinais “-”, vermelhos, aparecerá do lado esquerdo dos elementos da lista. Toque em um deles e aparecerá uma barra “Delete” para perguntar se você quer mesmo deletar. É só tocar nela que o elemento será deletado.

Agora, como fazer para mudar o dispositivo que o iOS Simulator está simulando? Bem, lá no alto da Janela do Xcode, no canto superior esquerdo, tem um botão para fazer isso, ao lado do nome da sua app:



Abra o arquivo “Main.storyboard”. Temos:
  1. Split View Controller;
  2. Dois Navigation Controllers;
  3. Um Table View Controller, para o Mestre;
  4. Um View Controller, para o Detalhe.



E, nas classes do projeto, temos:
  • AppDelegate.swift: Onde ele faz referência aos View Controllers e ao Managed Object Context;
  • MasterViewController: Onde ele gerencia o Table View Controller, ou seja o Mestre;
  • DetailViewController: Onde ele gerencia o View Controller de detalhe.



Para começar, vamos abrir o código do AppDelegate, e eu vou comentar algumas partes. De início, temos o método “didFinishLaunchingWithOptions”, que é invocado quando a app está para terminar de ser carregada e vai mostrar a primeira tela:


  func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    // Override point for customization after application launch.
    let splitViewController = self.window!.rootViewController as UISplitViewController
    let navigationController = splitViewController.viewControllers[splitViewController.viewControllers.count-1] as UINavigationController
    navigationController.topViewController.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem()
    splitViewController.delegate = self
    let masterNavigationController = splitViewController.viewControllers[0] as UINavigationController
    let controller = masterNavigationController.topViewController as MasterViewController
    controller.managedObjectContext = self.managedObjectContext
    return true
  }

Aqui, ele está pegando referências para o Split View Controller (“splitViewController”), de modo a colocar a classe AppDelegate como delegate dele, de modo a receber seus eventos. E também está acresentando um botão para alterar o modo do Split View Controller (de Detalhe para Mestre), no View Controller da cena de detalhes (é aquele botão “<Master”).
Finalmente, ele associa o seu Managed Object Context à propriedade “managedObjectContext”, do View Controller Mestre. Assim, o Mestre poderá acessar os objetos Core Data que queremos ler e gravar.



Ao final, ele lida com o Managed Object Context, criando um banco SQLite no diretório “Documents”, da app. Ele lida diretamente com URL, ao invés de path. Primeiro, ele tem uma cria “applicationDocumentsDirectory”, que aponta para o path onde será criado o Banco:



lazy var applicationDocumentsDirectory: NSURL = {
// The directory the application uses to store the Core Data store file. This code uses a directory named "com.obomprogramador.testeswift.MasterDetail" in the application's documents Application Support directory.
let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
return urls[urls.count-1] as NSURL
}()


Esta propriedade tem um “Getter”, sendo uma propriedade derivada. Ao ser invocada, o código será executado. Note que ela foi declarada como “lazy”, ou seja, só será calculada na primeira vez em que for solicitada.



O AppDelegate cria uma propriedade Persistent Store Coordinator, que indica o mecanismo que efetivamente vai gerenciar o armazenamento dos dados. Também é uma propriedade “lazy”, com um código “Getter”:



  lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator? = {
      // The persistent store coordinator for the application. This implementation creates and return a coordinator, having added the store for the application to it. This property is optional since there are legitimate error conditions that could cause the creation of the store to fail.
      // Create the coordinator and store
      var coordinator: NSPersistentStoreCoordinator? = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
      let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("MasterDetail.sqlite")
      var error: NSError? = nil
      var failureReason = "There was an error creating or loading the application's saved data."
      if coordinator!.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: url, options: nil, error: &error) == nil {
          coordinator = nil
          // Report any error we got.
          var dict = [String: AnyObject]()
          dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data"
          dict[NSLocalizedFailureReasonErrorKey] = failureReason
          dict[NSUnderlyingErrorKey] = error
          error = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)
          // Replace this with code to handle the error appropriately.
          // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
          NSLog("Unresolved error \(error), \(error!.userInfo)")
          abort()
      }
      return coordinator
  }()

No “Getter” dessa propriedade, o AppDelegate indica o tipo de Coordinator a ser usado pelo Core Data, no caso SQLite, e também indica o caminho onde o banco SQLite será armazenado.



Finalmente, o AppDelegate cria uma propriedade “lazy”, com “Getter”, para o Managed Object Context:


  lazy var managedObjectContext: NSManagedObjectContext? = {
      // Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.) This property is optional since there are legitimate error conditions that could cause the creation of the context to fail.
      let coordinator = self.persistentStoreCoordinator
      if coordinator == nil {
          return nil
      }
      var managedObjectContext = NSManagedObjectContext()
      managedObjectContext.persistentStoreCoordinator = coordinator
      return managedObjectContext
  }()

Note que essa propriedade usa a “persistentStoreCoordinator”.



O MasterViewController.swift é um pouco mais complicado. Afinal, além de lidar com o AppDelegate e o Managed Object Context, ele é o delegate da Table View e também é sua fonte de dados. Para começar, no “viewDidLoad()” ele obtém referência para o Master View Controller, e cria um botão para adicionar novos itens à Barra de botões:


  override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.
    self.navigationItem.leftBarButtonItem = self.editButtonItem()
    let addButton = UIBarButtonItem(barButtonSystemItem: .Add, target: self, action: "insertNewObject:")
    self.navigationItem.rightBarButtonItem = addButton
    if let split = self.splitViewController {
        let controllers = split.viewControllers
        self.detailViewController = controllers[controllers.count-1].topViewController as? DetailViewController
    }
  }

Cara! E como é que a Table View é carragada? Quando eu vou ler e carregar as células da tabela? Calma! Isso tudo é feito pelos métodos do protocolo “UITableViewDataSource”, que a nossa classe implementa. Ela é derivada de “UITableViewController”, logo, já traz os métodos:



  • tableView(_:cellForRowAtIndexPath:) - Retorna uma célula para uma determinada linha da tabela. É chamado quando é necessário incluir uma célula na Table View;
  • numberOfSectionsInTableView(_:) - Retorna o número de seções existentes em nossa Table View. Uma sessão é uma lista independente de dados. O normal é termos uma só seção;
  • tableView(_:numberOfRowsInSection:) - Retorna a quantidade de linhas existentes em uma sessão da Table View;
  • tableView(_:commitEditingStyle:forRowAtIndexPath:) - Efetua a alteração na Table View, seja para inserir ou deletar elementos;
  • tableView(_:canEditRowAtIndexPath:) - Indica se uma das linhas da Table View é editável ou não.



No momento em que a Table View pergunta a quantidade de seções, o método faz referência a uma propriedade “fetchedResultsController”:



override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return self.fetchedResultsController.sections?.count ?? 0
}



Neste momento, a propriedade “fetchedResultsController” será carregada pelo seu “Getter”:



  var fetchedResultsController: NSFetchedResultsController {
      if _fetchedResultsController != nil {
          return _fetchedResultsController!
      }
      let fetchRequest = NSFetchRequest()
      // Edit the entity name as appropriate.
      let entity = NSEntityDescription.entityForName("Event", inManagedObjectContext: self.managedObjectContext!)
      fetchRequest.entity = entity
      // Set the batch size to a suitable number.
      fetchRequest.fetchBatchSize = 20
      // Edit the sort key as appropriate.
      let sortDescriptor = NSSortDescriptor(key: "timeStamp", ascending: false)
      let sortDescriptors = [sortDescriptor]
      fetchRequest.sortDescriptors = [sortDescriptor]
      // Edit the section name key path and cache name if appropriate.
      // nil for section name key path means "no sections".
      let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.managedObjectContext!, sectionNameKeyPath: nil, cacheName: "Master")
      aFetchedResultsController.delegate = self
      _fetchedResultsController = aFetchedResultsController
   var error: NSError? = nil
   if !_fetchedResultsController!.performFetch(&error) {
        // Replace this implementation with code to handle the error appropriately.
        // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 
           //println("Unresolved error \(error), \(error.userInfo)")
        abort()
   }
      return _fetchedResultsController!
  }  

Não vou entrar em detalhes agora, mas o tipo dessa propriedade, “NSFetchedResultsController”, serve como intermediário entre o Core Data e a Table View, usando os resultados de um “Fetch”, feito no Core Data, para popular uma Table View. E também coordena a inserção, deleção e alteração de elementos em ambas (Core Data e Table View). Nosso View Controller também será seu “delegate”, logo, processaremos seus eventos.



Ok. Eu sei que parece meio confuso, mas a ideia aqui é só mostrar como uma app Mestre / Detalhe funciona, em linhas gerais. No próximo capítulo, vamos construir uma app usando esse template, e então as coisas vão ficar mais claras.



Antes de terminarmos, gostaria de mostrar que ele tem um Managed Object Model, criado com apenas uma entidade. Vamos abrir o arquivo “MasterDetail.xcdatamodeld”:







Ele só tem uma entidade “Entity”, com uma propriedade, “timeStamp”. E não foram criadas classes derivadas de NSManagedObject.



Conclusão




O objetivo deste capítulo foi mostrar como podemos navegar entre cenas, apresentando desde a navegação mais simples, até a mais complexa, que é a Master / Detail.



A API do Cocoa Touch, que é a interface gráfica do iOS, é muito rica, logo, seu aprendizado requer muita paciência e experimentação.


Não se esqueça!

Acesse a página do curso para ver as outras lições, e sempre baixe novamente o zip do curso, pois, como é um trabalho em andamento, pode haver correções de erros e aprimoramentos.

Se tiver dúvidas, use o fórum!

Esse "curso" não dá diploma algum! E todo o material é liberado sob licença "Creative Commons" compartilha igual.

Você pode compartilhar esse material da forma que desejar, desde que mantenha o mesmo tipo de licença e as atribuições de autoria original.


O trabalho "Criando apps para iPhone com Swift " de Cleuton Sampaio de Melo Jr está licenciado com uma Licença Creative Commons - Atribuição-CompartilhaIgual 4.0 Internacional. Isso inclui: Textos, páginas, gráficos e código-fonte.





Nenhum comentário:

Postar um comentário