Skip to content

Commit

Permalink
Contact collection large text accessibility support, bug fixes and sm…
Browse files Browse the repository at this point in the history
…all size (microsoft#193)

* added small size for colletion view

* Improvements

* size fix

* Large text AX support

* Refactoring

* more explicit comment

Co-authored-by: Mathieu Kavalec <[email protected]>
  • Loading branch information
MathieuKavalec and Mathieu Kavalec authored Aug 17, 2020
1 parent d38fa9c commit fe45056
Show file tree
Hide file tree
Showing 6 changed files with 385 additions and 123 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,97 @@
import FluentUI
import UIKit

class ContactCollectionViewDemoController: UIViewController {
private let contactCollectionView = ContactCollectionView()

class ContactCollectionViewDemoController: DemoController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = Colors.surfacePrimary
view.backgroundColor = Colors.surfaceSecondary

let largeTitleLabel = createlabel(text: "Large")
scrollingContainer.addSubview(largeTitleLabel)

let largeCollectionView = ContactCollectionView(personas: personas)
largeCollectionView.contactCollectionViewDelegate = self
largeCollectionView.translatesAutoresizingMaskIntoConstraints = false
scrollingContainer.addSubview(largeCollectionView)

contactCollectionView.contactCollectionViewDelegate = self
contactCollectionView.translatesAutoresizingMaskIntoConstraints = false
contactCollectionView.contactList = samplePersonas
let smallTitleLabel = createlabel(text: "Small")
scrollingContainer.addSubview(smallTitleLabel)

view.addSubview(contactCollectionView)
let smallCollectionView = ContactCollectionView(size: .small, personas: personas)
smallCollectionView.contactCollectionViewDelegate = self
smallCollectionView.translatesAutoresizingMaskIntoConstraints = false
scrollingContainer.addSubview(smallCollectionView)

let stackView = UIStackView(frame: .zero)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = Constants.spacing
stackView.alignment = .center
scrollingContainer.addSubview(stackView)

stackView.addArrangedSubview(createlabel(text: "Large"))
stackView.addArrangedSubview(ContactView(identifier: "Kat Larrson"))
stackView.addArrangedSubview(ContactView(title: "Kristin", subtitle: "Patterson"))
stackView.addArrangedSubview(createlabel(text: "Small"))
stackView.addArrangedSubview(ContactView(identifier: "Kat Larrson", size: .small))
stackView.addArrangedSubview(ContactView(title: "Kristin", subtitle: "Patterson", size: .small))

NSLayoutConstraint.activate([
contactCollectionView.topAnchor.constraint(equalTo: view.topAnchor, constant: 20),
contactCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
contactCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
largeTitleLabel.topAnchor.constraint(equalTo: scrollingContainer.topAnchor, constant: Constants.spacing),
largeTitleLabel.leadingAnchor.constraint(equalTo: scrollingContainer.leadingAnchor, constant: Constants.leadingSpacing),
largeCollectionView.topAnchor.constraint(equalTo: largeTitleLabel.bottomAnchor, constant: Constants.spacing),
largeCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
largeCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
smallTitleLabel.topAnchor.constraint(equalTo: largeCollectionView.bottomAnchor, constant: Constants.spacing),
smallTitleLabel.leadingAnchor.constraint(equalTo: scrollingContainer.leadingAnchor, constant: Constants.leadingSpacing),
smallCollectionView.topAnchor.constraint(equalTo: smallTitleLabel.bottomAnchor, constant: Constants.spacing),
smallCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
smallCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
stackView.leadingAnchor.constraint(equalTo: scrollingContainer.leadingAnchor, constant: Constants.leadingSpacing),
stackView.topAnchor.constraint(equalTo: smallCollectionView.bottomAnchor, constant: Constants.spacing),
stackView.bottomAnchor.constraint(equalTo: scrollingContainer.bottomAnchor)
])
}

private func createlabel(text: String) -> Label {
let label = Label(style: .title2)
label.translatesAutoresizingMaskIntoConstraints = false
label.text = text

return label
}

private struct Constants {
static let spacing: CGFloat = 20
static let leadingSpacing: CGFloat = 30
}

private let personas: [PersonaData] = [
PersonaData(firstName: "Kat", lastName: "Larrson", avatarImage: UIImage(named: "avatar_kat_larsson")),
PersonaData(firstName: "Ashley", lastName: "McCarthy", avatarImage: UIImage(named: "avatar_ashley_mccarthy")),
PersonaData(firstName: "Allan", lastName: "Munger", avatarImage: UIImage(named: "avatar_allan_munger")),
PersonaData(firstName: "Amanda", lastName: "Brady", avatarImage: UIImage(named: "avatar_amanda_brady")),
PersonaData(firstName: "Kevin", lastName: "Sturgis"),
PersonaData(firstName: "Lydia", lastName: "Bauer", avatarImage: UIImage(named: "avatar_lydia_bauer")),
PersonaData(firstName: "Robin", lastName: "Counts"),
PersonaData(firstName: "Tim", lastName: "Deboer", avatarImage: UIImage(named: "avatar_tim_deboer")),
PersonaData(firstName: "Daisy", lastName: "Phillips", avatarImage: UIImage(named: "avatar_daisy_phillips")),
PersonaData(firstName: "Mona", lastName: "Kane", email: "[email protected]"),
PersonaData(firstName: "Elvia", lastName: "Atkins", avatarImage: UIImage(named: "avatar_elvia_atkins")),
PersonaData(firstName: "Johnie", lastName: "McConnell", subtitle: "Designer", avatarImage: UIImage(named: "avatar_johnie_mcconnell")),
PersonaData(firstName: "Charlotte", lastName: "Waltsson"),
PersonaData(firstName: "Mauricio", lastName: "August", avatarImage: UIImage(named: "avatar_mauricio_august")),
PersonaData(firstName: "Robert", lastName: "Tolbert", avatarImage: UIImage(named: "avatar_robert_tolbert")),
PersonaData(firstName: "Isaac", lastName: "Fielder", avatarImage: UIImage(named: "avatar_isaac_fielder")),
PersonaData(firstName: "Carole", lastName: "Poland"),
PersonaData(firstName: "Elliot", lastName: "Woodward"),
PersonaData(firstName: "Henry", lastName: "Brill", avatarImage: UIImage(named: "avatar_henry_brill")),
PersonaData(firstName: "Cecil", lastName: "Folk", avatarImage: UIImage(named: "avatar_cecil_folk")),
PersonaData(name: "Katri Ahokas", avatarImage: UIImage(named: "avatar_katri_ahokas")),
PersonaData(name: "Colin Ballinger", email: "[email protected]", avatarImage: UIImage(named: "avatar_colin_ballinger")),
PersonaData(email: "[email protected]"),
PersonaData(email: "[email protected]", subtitle: "Software Engineer")
]
}

extension ContactCollectionViewDemoController: ContactCollectionViewDelegate {
Expand Down
151 changes: 110 additions & 41 deletions ios/FluentUI/People Picker/ContactCollectionView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,57 @@ public protocol ContactCollectionViewDelegate: AnyObject {

@objc(MSFContactCollectionView)
open class ContactCollectionView: UICollectionView {
@objc(MSFContactCollectionViewSize)
public enum Size: Int {
case large
case small

var contactViewSize: ContactView.Size {
switch self {
case .large:
return .large
case .small:
return .small
}
}

var avatarSize: AvatarSize {
switch self {
case .large:
return .extraExtraLarge
case .small:
return .extraLarge
}
}

var width: CGFloat {
return contactViewSize.width
}
}

/// The size of the collection view.
@objc public let size: Size

/// Initializes the collection view by setting the layout, constraints, and cell to be used.
///
/// - Parameters:
/// - personaData: Array of PersonaData used to create each individual ContactView
@objc public init(personaData: [PersonaData] = []) {
layout = ContactCollectionViewLayout()
/// - Parameter size: The size of the collection view.
/// - Parameter personas: Array of PersonaData used to create each individual ContactView.
@objc public init(size: Size = .large, personas: [PersonaData] = []) {
layout = ContactCollectionViewLayout(size: size)
layout.scrollDirection = .horizontal
self.size = size
self.personas = personas
super.init(frame: .zero, collectionViewLayout: layout)

if personaData.count > 0 {
contactList = personaData
}
register(ContactCollectionViewCell.self, forCellWithReuseIdentifier: ContactCollectionViewCell.identifier)

heightConstraint.isActive = true
configureCollectionView()
setupHeightConstraint()
register(ContactCollectionViewCell.self, forCellWithReuseIdentifier: ContactCollectionViewCell.identifier)
NotificationCenter.default.addObserver(self, selector: #selector(setupHeightConstraint), name: UIContentSizeCategory.didChangeNotification, object: nil)
updateHeightConstraint()

NotificationCenter.default.addObserver(self,
selector: #selector(sizeCategoryDidUpdate),
name: UIContentSizeCategory.didChangeNotification,
object: nil)
}

@objc public required init?(coder: NSCoder) {
Expand All @@ -45,27 +77,41 @@ open class ContactCollectionView: UICollectionView {

/// The array of PersonaData which is used to create each ContactView.
/// The height constraint of the ContactCollectionView is updated when the count increases from 0 or decreases to 0.
@objc public var contactList: [PersonaData] = [] {
@objc public var personas: [PersonaData] = [] {
didSet {
if (oldValue.count == 0 && contactList.count > 0) || (oldValue.count > 0 && contactList.count == 0) {
setupHeightConstraint()
if (oldValue.count == 0 && personas.count > 0) || (oldValue.count > 0 && personas.count == 0) {
updateHeightConstraint()
}
}
}

open weak var contactCollectionViewDelegate: ContactCollectionViewDelegate?
open weak var contactCollectionViewDelegate: ContactCollectionViewDelegate? {
didSet {
if oldValue == nil && contactCollectionViewDelegate != nil {
let cells = visibleCells as! [ContactCollectionViewCell]
for cell in cells {
cell.contactView?.contactViewDelegate = self
}
}
}
}

private func configureCollectionView() {
translatesAutoresizingMaskIntoConstraints = false
showsHorizontalScrollIndicator = false
showsVerticalScrollIndicator = false
backgroundColor = Colors.surfacePrimary
dataSource = self
contentInset = UIEdgeInsets(top: 0, left: Constants.leadingInset, bottom: 0, right: 0)
contentInset = UIEdgeInsets(top: Constants.verticalInset, left: Constants.horizontalInset, bottom: Constants.verticalInset, right: Constants.horizontalInset)
}

@objc private func setupHeightConstraint() {
let height = (contactList.count > 0) ? UIApplication.shared.preferredContentSizeCategory.contactHeight : 0
@objc private func sizeCategoryDidUpdate() {
updateHeightConstraint()
reloadData()
}

@objc private func updateHeightConstraint() {
let height = UIApplication.shared.preferredContentSizeCategory.contactHeight(size: size.contactViewSize) + 2 * Constants.verticalInset
heightConstraint.constant = height
}

Expand All @@ -87,9 +133,9 @@ open class ContactCollectionView: UICollectionView {
var offSet = contentOffset.x
if cellLeftPosition < viewLeadingPosition {
offSet = cellLeftPosition - extraScrollWidth
offSet = max(offSet, -Constants.leadingInset)
offSet = max(offSet, -Constants.horizontalInset)
} else if cellRightPosition > viewTrailingPosition {
let maxOffsetX = contentSize.width - frame.size.width + extraScrollWidth
let maxOffsetX = contentSize.width - frame.size.width + extraScrollWidth - Constants.horizontalInset
offSet = cellRightPosition - frame.size.width + extraScrollWidth
offSet = min(offSet, maxOffsetX)
}
Expand All @@ -100,15 +146,17 @@ open class ContactCollectionView: UICollectionView {
}

private struct Constants {
static let leadingInset: CGFloat = 16.0
static let amountOfNextContactToShow: CGFloat = 20.0
static let horizontalInset: CGFloat = 16
static let verticalInset: CGFloat = 8
static let amountOfNextContactToShow: CGFloat = 20
}

private let layout: ContactCollectionViewLayout

private lazy var heightConstraint: NSLayoutConstraint = {
let heightConstraint = heightAnchor.constraint(equalToConstant: 0.0)
return heightConstraint
return heightAnchor.constraint(equalToConstant: 0)
}()

private var contactViewToIndexMap: [ContactView: Int] = [:]
}

Expand All @@ -117,47 +165,68 @@ extension ContactCollectionView: ContactViewDelegate {
if let contactCollectionViewDelegate = contactCollectionViewDelegate, let currentTappedIndex = contactViewToIndexMap[contact] {
let indexPath = IndexPath(item: currentTappedIndex, section: 0)
scrollToContact(at: indexPath)
contactCollectionViewDelegate.didTapOnContactViewAtIndex?(index: currentTappedIndex, personaData: contactList[currentTappedIndex])
contactCollectionViewDelegate.didTapOnContactViewAtIndex?(index: currentTappedIndex, personaData: personas[currentTappedIndex])
}
}
}

extension ContactCollectionView: UICollectionViewDataSource {
@objc public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return contactList.count
return personas.count
}

@objc public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ContactCollectionViewCell.identifier, for: indexPath) as! ContactCollectionViewCell
cell.setup(contact: contactList[indexPath.item])
cell.contactView.contactViewDelegate = self
contactViewToIndexMap[cell.contactView] = indexPath.item
cell.setup(contact: personas[indexPath.item], size: size.contactViewSize)

if contactCollectionViewDelegate != nil {
cell.contactView!.contactViewDelegate = self
}

contactViewToIndexMap[cell.contactView!] = indexPath.item

return cell
}
}

extension UIContentSizeCategory {
var contactHeight: CGFloat {
func contactHeight(size: ContactView.Size) -> CGFloat {
var height: CGFloat = 0

switch self {
case .extraSmall:
return 115.0
height = 117
case .small:
return 117.0
height = 119
case .medium:
return 118.0
height = 120
case .large:
return 121.0
height = 123
case .extraLarge:
return 125.0
height = 127
case .extraExtraLarge:
return 129.0
case .extraExtraExtraLarge, .accessibilityMedium,
.accessibilityLarge, .accessibilityExtraLarge,
.accessibilityExtraExtraLarge, .accessibilityExtraExtraExtraLarge:
return 135.0
height = 131
case .extraExtraExtraLarge:
height = 137
case .accessibilityMedium:
height = 141
case .accessibilityLarge:
height = 151
case .accessibilityExtraLarge:
height = 165
case .accessibilityExtraExtraLarge:
height = 178
case .accessibilityExtraExtraExtraLarge:
height = 194
default:
return 135.0
break
}

if size == .small {
// Remove height for smaller size to compensate for the missing secondary label.
height -= 38
}

return height
}
}
22 changes: 13 additions & 9 deletions ios/FluentUI/People Picker/ContactCollectionViewCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,35 @@ import UIKit

class ContactCollectionViewCell: UICollectionViewCell {
static let identifier: String = "ContactCollectionViewCell"
public var contactView: ContactView
public var contactView: ContactView?

override init(frame: CGRect) {
contactView = ContactView(title: "", subtitle: "")
super.init(frame: frame)
}

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

func setup(contact persona: PersonaData) {
let identifier = (persona.name.count > 0) ? persona.name : persona.email
contactView = ContactView(identifier: identifier)
contactView.translatesAutoresizingMaskIntoConstraints = false
func setup(contact persona: PersonaData, size: ContactView.Size) {
if let name = persona.composedName {
contactView = ContactView(title: name.0, subtitle: name.1, size: size)
} else {
let identifier = (persona.name.count > 0) ? persona.name : persona.email
contactView = ContactView(identifier: identifier, size: size)
}

contactView!.translatesAutoresizingMaskIntoConstraints = false

if let avatarImage = persona.avatarImage {
contactView.avatarImage = avatarImage
contactView!.avatarImage = avatarImage
}

contentView.addSubview(contactView)
contentView.addSubview(contactView!)
}

override func prepareForReuse() {
contactView.removeAllSubviews()
contactView?.removeAllSubviews()
super.prepareForReuse()
}
}
Loading

0 comments on commit fe45056

Please sign in to comment.