
Tinder - todos sabemos que este é um aplicativo de namoro em que você pode simplesmente rejeitar ou aceitar alguém deslizando para a esquerda ou para a direita. Essa idéia do leitor de cartão agora é usada em inúmeras aplicações. Essa maneira de exibir dados é para você se você estiver cansado de usar as visualizações de tabela e coleção. Existem muitos livros sobre esse assunto, mas esse projeto me levou muito tempo.
Você pode ver o projeto completo no meu
github .
Antes de tudo, gostaria de prestar homenagem ao
post de Phill Farrugia sobre esse assunto e, em seguida, à série do
YouTube no estúdio Big Mountain sobre um assunto semelhante. Então, como fazemos essa interface? Eu recebi ajuda para publicar Phil sobre esse tópico. Essencialmente, a ideia é criar UIViews e inseri-las como subvisões na visualização do contêiner. Então, usando o índice, forneceremos a cada UIView alguma inserção horizontal e vertical e alteraremos ligeiramente sua largura. Além disso, quando arrastamos um dedo por um mapa, todos os quadros das visualizações serão reorganizados de acordo com o novo valor do índice.
Começaremos criando uma exibição de contêiner em um simples ViewController.
class ViewController: UIViewController {
Como você pode ver, criei minha própria classe chamada SwipeContainerView e apenas configurei o stackViewContainer usando restrições automáticas. Nada para se preocupar. O tamanho do SwipeContainerView será 300x400 e será centralizado no eixo X e apenas 60 pixels acima do meio do eixo Y.
Agora que configuramos o stackContainer, iremos para a subclasse de StackContainerView e carregaremos todos os tipos de mapas nele. Antes disso, criaremos um protocolo que terá três métodos:
protocol SwipeCardsDataSource { func numberOfCardsToShow() -> Int func card(at index: Int) -> SwipeCardView func emptyView() -> UIView? }
Pense neste protocolo como um TableViewDataSource. A conformidade de nossa classe ViewController com este protocolo permitirá a transferência de informações sobre nossos dados para a classe SwipeCardContainer. Tem três métodos:
numberOfCardsToShow () -> Int
: retorna o número de cartões que precisamos mostrar. É apenas um contador de matriz de dados.card(at index: Int) -> SwipeCardView
: retorna SwipeCardView (criaremos esta classe em um momento)EmptyView
-> Não faremos nada com ele, mas assim que todos os cartões forem excluídos, chamar esse método delegado retornará uma exibição vazia com alguma mensagem (não implementarei isso nesta lição específica, tente você mesmo)
Alinhe o controlador de exibição com este protocolo:
extension ViewController : SwipeCardsDataSource { func numberOfCardsToShow() -> Int { return viewModelData.count } func card(at index: Int) -> SwipeCardView { let card = SwipeCardView() card.dataSource = viewModelData[index] return card } func emptyView() -> UIView? { return nil } }
O primeiro método retornará o número de elementos na matriz de dados. No segundo método, crie uma nova instância SwipeCardView () e envie os dados da matriz para esse índice e, em seguida, retorne a instância SwipeCardView.
SwipeCardView é uma subclasse do UIView que possui UIImage, UILabel e um reconhecedor de gestos. Mais sobre isso mais tarde. Usaremos este protocolo para se comunicar com a apresentação do contêiner.
stackContainer.dataSource = self
Quando o código acima é acionado, a função reloadData é chamada e, em seguida, chama essas funções de fonte de dados.
Class StackViewContainer: UIView { . . var dataSource: SwipeCardsDataSource? { didSet { reloadData() } } ....
Função ReloadData:
func reloadData() { guard let datasource = dataSource else { return } setNeedsLayout() layoutIfNeeded() numberOfCardsToShow = datasource.numberOfCardsToShow() remainingcards = numberOfCardsToShow for i in 0..<min(numberOfCardsToShow,cardsToBeVisible) { addCardView(cardView: datasource.card(at: i), atIndex: i ) } }
Na função reloadData, primeiro obtemos o número de cartões e o armazenamos na variável numberOfCardsToShow. Em seguida, atribuímos isso a outra variável chamada restanteCartões. No loop for, criamos um mapa que é uma instância do SwipeCardView usando o valor do índice.
for i in 0..<min(numberOfCardsToShow,cardsToBeVisible) { addCardView(cardView: datasource.card(at: i), atIndex: i ) }
De fato, queremos que menos de três cartas apareçam por vez. Portanto, usamos a função min. CardsToBeVisible é uma constante igual a 3. Se numberOfToShow for maior que 3, apenas três cartões serão exibidos. Criamos esses cartões a partir do protocolo:
func card(at index: Int) -> SwipeCardView
A função addCardView () é simplesmente usada para inserir mapas como sub-visualizações.
private func addCardView(cardView: SwipeCardView, atIndex index: Int) { cardView.delegate = self addCardFrame(index: index, cardView: cardView) cardViews.append(cardView) insertSubview(cardView, at: 0) remainingcards -= 1 }
Nesta função, adicionamos cardView à hierarquia de visualizações e, adicionando cartões como uma subview, reduzimos os cartões restantes em 1. Assim que adicionamos cardView como uma subview, definimos o quadro desses cartões. Para fazer isso, usamos outra função addCardFrame ():
func addCardFrame(index: Int, cardView: SwipeCardView) { var cardViewFrame = bounds let horizontalInset = (CGFloat(index) * self.horizontalInset) let verticalInset = CGFloat(index) * self.verticalInset cardViewFrame.size.width -= 2 * horizontalInset cardViewFrame.origin.x += horizontalInset cardViewFrame.origin.y += verticalInset cardView.frame = cardViewFrame }
Essa lógica addCardFrame () é obtida diretamente da postagem de Phil. Aqui, definimos o quadro do mapa de acordo com seu índice. O primeiro cartão com o índice 0 terá um quadro, como um contêiner. Em seguida, alteramos a origem do quadro e a largura do mapa de acordo com a inserção. Assim, adicionamos o cartão um pouco à direita do cartão acima, reduzimos sua largura e também puxamos os cartões para baixo para criar a sensação de que os cartões são empilhados um sobre o outro.
Feito isso, você verá que as cartas estão empilhadas umas sobre as outras. Muito bom!

No entanto, agora precisamos adicionar um gesto de furto na visualização do mapa. Vamos agora voltar nossa atenção para a classe SwipeCardView.
SwipeCardView
A classe swipeCardView é uma subclasse regular do UIView. No entanto, por razões conhecidas apenas pelos engenheiros da Apple, é incrivelmente difícil adicionar sombras a um UIView com um canto arredondado. Para adicionar sombras às visualizações do mapa, crio duas UIViews. Um deles é o shadowView e, em seguida, deslize-o. Essencialmente, o shadowView tem uma sombra e é isso. O SwipeView possui cantos arredondados. No swipeView, adicionei o UIImageView, um UILabel para mostrar dados e imagens.
var swipeView : UIView! var shadowView : UIView!
Definir shadowView e swipeView:
func configureShadowView() { shadowView = UIView() shadowView.backgroundColor = .clear shadowView.layer.shadowColor = UIColor.black.cgColor shadowView.layer.shadowOffset = CGSize(width: 0, height: 0) shadowView.layer.shadowOpacity = 0.8 shadowView.layer.shadowRadius = 4.0 addSubview(shadowView) shadowView.translatesAutoresizingMaskIntoConstraints = false shadowView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true shadowView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true shadowView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true shadowView.topAnchor.constraint(equalTo: topAnchor).isActive = true } func configureSwipeView() { swipeView = UIView() swipeView.layer.cornerRadius = 15 swipeView.clipsToBounds = true shadowView.addSubview(swipeView) swipeView.translatesAutoresizingMaskIntoConstraints = false swipeView.leftAnchor.constraint(equalTo: shadowView.leftAnchor).isActive = true swipeView.rightAnchor.constraint(equalTo: shadowView.rightAnchor).isActive = true swipeView.bottomAnchor.constraint(equalTo: shadowView.bottomAnchor).isActive = true swipeView.topAnchor.constraint(equalTo: shadowView.topAnchor).isActive = true }
Em seguida, adicionei um reconhecedor de gestos a esse tipo de cartão e a função seletora é chamada após o reconhecimento. Esta função seletora possui muita lógica para rolagem, inclinação, etc. Vamos ver:
@objc func handlePanGesture(sender: UIPanGestureRecognizer){ let card = sender.view as! SwipeCardView let point = sender.translation(in: self) let centerOfParentContainer = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2) card.center = CGPoint(x: centerOfParentContainer.x + point.x, y: centerOfParentContainer.y + point.y) switch sender.state { case .ended: if (card.center.x) > 400 { delegate?.swipeDidEnd(on: card) UIView.animate(withDuration: 0.2) { card.center = CGPoint(x: centerOfParentContainer.x + point.x + 200, y: centerOfParentContainer.y + point.y + 75) card.alpha = 0 self.layoutIfNeeded() } return }else if card.center.x < -65 { delegate?.swipeDidEnd(on: card) UIView.animate(withDuration: 0.2) { card.center = CGPoint(x: centerOfParentContainer.x + point.x - 200, y: centerOfParentContainer.y + point.y + 75) card.alpha = 0 self.layoutIfNeeded() } return } UIView.animate(withDuration: 0.2) { card.transform = .identity card.center = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2) self.layoutIfNeeded() } case .changed: let rotation = tan(point.x / (self.frame.width * 2.0)) card.transform = CGAffineTransform(rotationAngle: rotation) default: break } }
As quatro primeiras linhas no código acima:
let card = sender.view as! SwipeCardView let point = sender.translation(in: self) let centerOfParentContainer = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2) card.center = CGPoint(x: centerOfParentContainer.x + point.x, y: centerOfParentContainer.y + point.y)
Primeiro, temos a idéia pela qual o gesto foi realizado. Em seguida, usamos o método de transferência para descobrir quantas vezes o usuário atingiu o cartão. A terceira linha essencialmente obtém o ponto médio do contêiner pai. A última linha em que instalamos o card.center. Quando o usuário passa o dedo sobre o cartão, o centro do cartão é aumentado pelo valor traduzido de x e pelo valor traduzido de y. Para obter esse comportamento de snap, alteramos significativamente o ponto central do mapa de coordenadas fixas. Quando a tradução dos gestos termina, retornamos ao card.center.
No caso de state.ended:
if (card.center.x) > 400 { delegate?.swipeDidEnd(on: card) UIView.animate(withDuration: 0.2) { card.center = CGPoint(x: centerOfParentContainer.x + point.x + 200, y: centerOfParentContainer.y + point.y + 75) card.alpha = 0 self.layoutIfNeeded() } return }else if card.center.x < -65 { delegate?.swipeDidEnd(on: card) UIView.animate(withDuration: 0.2) { card.center = CGPoint(x: centerOfParentContainer.x + point.x - 200, y: centerOfParentContainer.y + point.y + 75) card.alpha = 0 self.layoutIfNeeded() } return }
Verificamos se card.center.x é maior que 400 ou se card.center.x é menor que -65. Nesse caso, descartamos esses cartões, alterando o centro.
Se deslize para a direita:
card.center = CGPoint(x: centerOfParentContainer.x + point.x + 200, y: centerOfParentContainer.y + point.y + 75)
Se deslizar para a esquerda:
card.center = CGPoint(x: centerOfParentContainer.x + point.x - 200, y: centerOfParentContainer.y + point.y + 75)
Se o usuário terminar o gesto no meio entre 400 e -65, redefiniremos o centro do mapa. Também chamamos o método delegate quando o furto termina. Mais sobre isso mais tarde.
Para obter essa inclinação quando você desliza o mapa; Serei brutalmente honesto. Usei um pouco de geometria e usei valores diferentes da perpendicular e da base e, em seguida, usei a função tan para obter o ângulo de rotação. Novamente, isso foi apenas tentativa e erro. Usar point.x e a largura do contêiner como dois perímetros pareceu funcionar bem. Sinta-se livre para experimentar esses valores.
case .changed: let rotation = tan(point.x / (self.frame.width * 2.0)) card.transform = CGAffineTransform(rotationAngle: rotation)
Agora vamos falar sobre a função delegar. Usaremos a função delegar para se comunicar entre SwipeCardView e ContainerView.
protocol SwipeCardsDelegate { func swipeDidEnd(on view: SwipeCardView) }
Essa função levará em conta o tipo em que o furto ocorreu e tomaremos várias etapas para removê-lo das subvisões e, em seguida, refazeremos todos os quadros dos cartões sob ele. Aqui está como:
func swipeDidEnd(on view: SwipeCardView) { guard let datasource = dataSource else { return } view.removeFromSuperview() if remainingcards > 0 { let newIndex = datasource.numberOfCardsToShow() - remainingcards addCardView(cardView: datasource.card(at: newIndex), atIndex: 2) for (cardIndex, cardView) in visibleCards.reversed().enumerated() { UIView.animate(withDuration: 0.2, animations: { cardView.center = self.center self.addCardFrame(index: cardIndex, cardView: cardView) self.layoutIfNeeded() }) } }else { for (cardIndex, cardView) in visibleCards.reversed().enumerated() { UIView.animate(withDuration: 0.2, animations: { cardView.center = self.center self.addCardFrame(index: cardIndex, cardView: cardView) self.layoutIfNeeded() }) } } }
Primeiro remova essa visualização da superavista. Feito isso, verifique se resta algum cartão. Se houver, criaremos um novo índice para o cartão a ser criado. Criaremos newIndex subtraindo o número total de cartões para mostrar com o restante dos cartões. Em seguida, adicionaremos o mapa como uma subvisão. No entanto, este novo cartão será o mais baixo, de modo que o 2 que enviamos garantirá essencialmente que o quadro adicionado corresponda ao índice 2 ou o mais baixo.
Para animar os quadros dos cartões restantes, usaremos o índice de subvisualização. Para fazer isso, criaremos uma matriz visibleCards, que conterá todas as subvisões do contêiner como uma matriz.
var visibleCards: [SwipeCardView] { return subviews as? [SwipeCardView] ?? [] }
O problema, no entanto, é que a matriz visibleCards terá um índice de subvisão invertida. Assim, o primeiro cartão será o terceiro, o segundo permanecerá em segundo lugar e o terceiro estará em primeira posição. Para evitar que isso aconteça, executaremos a matriz visibleCards na ordem inversa para obter o índice de subvisão real, e não como eles estão localizados na matriz visibleCards.
for (cardIndex, cardView) in visibleCards.reversed().enumerated() { UIView.animate(withDuration: 0.2, animations: { cardView.center = self.center self.addCardFrame(index: cardIndex, cardView: cardView) self.layoutIfNeeded() }) }
Então agora vamos atualizar os quadros do restante do cardViews.
Isso é tudo. Essa é uma maneira ideal de apresentar uma pequena quantidade de dados.