Merged PR 200173: Popup Menu implementation based on Drawer

I used our Drawer controller to implement Popup Menu. In Outlook presentation and content are coupled together for each of Popup Menu and Action Sheet. We separated presentation (Drawer) from content. This work builds on existing presentation code (Drawer) and adds content as Popup Menu. Public API is very similar to Outlook with some cleaning done. This was my FHL project :-)

Related work items: #653496
This commit is contained in:
Vlad Filyakov 2018-11-07 21:58:53 +00:00
Родитель 4646de3cb8
Коммит 322db14984
48 изменённых файлов: 1164 добавлений и 51 удалений

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 276 KiB

После

Ширина:  |  Высота:  |  Размер: 276 KiB

Просмотреть файл

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Просмотреть файл

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "Las Vegas@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Двоичные данные
OfficeUIFabric.Demo/OfficeUIFabric.Demo/Assets.xcassets/Cities/Las Vegas.imageset/Las Vegas@3x.png поставляемый Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 10 KiB

Просмотреть файл

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "Montreal@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Двоичные данные
OfficeUIFabric.Demo/OfficeUIFabric.Demo/Assets.xcassets/Cities/Montreal.imageset/Montreal@3x.png поставляемый Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 13 KiB

Просмотреть файл

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "Phoenix@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Двоичные данные
OfficeUIFabric.Demo/OfficeUIFabric.Demo/Assets.xcassets/Cities/Phoenix.imageset/Phoenix@3x.png поставляемый Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 6.9 KiB

Просмотреть файл

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "San Francisco@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 7.7 KiB

Просмотреть файл

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "Seattle@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Двоичные данные
OfficeUIFabric.Demo/OfficeUIFabric.Demo/Assets.xcassets/Cities/Seattle.imageset/Seattle@3x.png поставляемый Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 11 KiB

Просмотреть файл

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "Toronto@3x.jpg",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Двоичные данные
OfficeUIFabric.Demo/OfficeUIFabric.Demo/Assets.xcassets/Cities/Toronto.imageset/Toronto@3x.jpg поставляемый Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 3.4 KiB

Просмотреть файл

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "Vancouver@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Двоичные данные
OfficeUIFabric.Demo/OfficeUIFabric.Demo/Assets.xcassets/Cities/Vancouver.imageset/Vancouver@3x.png поставляемый Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 14 KiB

Просмотреть файл

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Просмотреть файл

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "agenda-25x25.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Двоичный файл не отображается.

Просмотреть файл

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "attach-25x25.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Двоичный файл не отображается.

Просмотреть файл

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "day-view-25x25.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Двоичный файл не отображается.

Просмотреть файл

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "flag-25x25.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Двоичные данные
OfficeUIFabric.Demo/OfficeUIFabric.Demo/Assets.xcassets/PopupMenu/flag-25x25.imageset/flag-25x25.pdf поставляемый Normal file

Двоичный файл не отображается.

Просмотреть файл

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "mail-unread-25x25.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Двоичный файл не отображается.

Просмотреть файл

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "month-view-25x25.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Двоичный файл не отображается.

Просмотреть файл

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "week-view-25x25.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Двоичный файл не отображается.

Просмотреть файл

@ -11,5 +11,6 @@ let demos: [(title: String, controllerClass: UIViewController.Type)] = [
("MSDatePicker", MSDatePickerDemoController.self),
("MSDrawerController", MSDrawerDemoController.self),
("MSLabel", MSLabelDemoController.self),
("MSPersonaListView", MSPersonaListViewDemoController.self)
("MSPersonaListView", MSPersonaListViewDemoController.self),
("MSPopupMenuController", MSPopupMenuDemoController.self)
]

Просмотреть файл

@ -5,12 +5,40 @@
import OfficeUIFabric
import UIKit
let samplePersonas: [MSPersonaData] = [
MSPersonaData(name: "Kat Larrson", email: "kat.larrson@contoso.com", subtitle: "Designer", avatarImage: UIImage(named: "avatar_kat_larsson")),
MSPersonaData(name: "Kristin Patterson", email: "kristin.patterson@contoso.com", subtitle: "Software Engineer"),
MSPersonaData(name: "Ashley McCarthy", avatarImage: UIImage(named: "avatar_ashley_mccarthy")),
MSPersonaData(name: "Carole Poland", email: "carole.poland@contoso.com", subtitle: "Software Engineer"),
MSPersonaData(name: "Allan Munger", email: "allan.munger@contoso.com", subtitle: "Designer", avatarImage: UIImage(named: "avatar_allan_munger")),
MSPersonaData(name: "Amanda Brady", subtitle: "Program Manager", avatarImage: UIImage(named: "avatar_amanda_brady")),
MSPersonaData(name: "Kevin Sturgis", email: "kevin.sturgis@contoso.com", subtitle: "Software Engineeer"),
MSPersonaData(name: "Lydia Bauer", email: "lydia.bauer@contoso.com", avatarImage: UIImage(named: "avatar_lydia_bauer")),
MSPersonaData(name: "Robin Counts", subtitle: "Program Manager", avatarImage: UIImage(named: "avatar_robin_counts")),
MSPersonaData(name: "Tim Deboer", email: "tim.deboer@contoso.com", subtitle: "Designer", avatarImage: UIImage(named: "avatar_tim_deboer")),
MSPersonaData(email: "wanda.howard@contoso.com", subtitle: "Director"),
MSPersonaData(name: "Daisy Phillips", email: "daisy.phillips@contoso.com", subtitle: "Software Engineer", avatarImage: UIImage(named: "avatar_daisy_phillips")),
MSPersonaData(name: "Katri Ahokas", subtitle: "Program Manager", avatarImage: UIImage(named: "avatar_katri_ahokas")),
MSPersonaData(name: "Colin Ballinger", email: "colin.ballinger@contoso.com", subtitle: "Software Engineer", avatarImage: UIImage(named: "avatar_colin_ballinger")),
MSPersonaData(name: "Mona Kane", email: "mona.kane@contoso.com", subtitle: "Designer"),
MSPersonaData(name: "Elvia Atkins", email: "elvia.atkins@contoso.com", subtitle: "Software Engineer", avatarImage: UIImage(named: "avatar_elvia_atkins")),
MSPersonaData(name: "Johnie McConnell", subtitle: "Designer", avatarImage: UIImage(named: "avatar_johnie_mcconnell")),
MSPersonaData(name: "Charlotte Waltsson", email: "charlotte.waltsson@contoso.com", subtitle: "Software Engineer"),
MSPersonaData(name: "Mauricio August", email: "mauricio.august@contoso.com", subtitle: "Program Manager", avatarImage: UIImage(named: "avatar_mauricio_august")),
MSPersonaData(name: "Robert Tolbert", email: "robert.tolbert@contoso.com", subtitle: "Software Engineer", avatarImage: UIImage(named: "avatar_robert_tolbert")),
MSPersonaData(name: "Isaac Fielder", subtitle: "Designer", avatarImage: UIImage(named: "avatar_isaac_fielder")),
MSPersonaData(name: "Elliot Woodward", subtitle: "Designer"),
MSPersonaData(email: "carlos.slattery@contoso.com", subtitle: "Software Engineer"),
MSPersonaData(name: "Henry Brill", subtitle: "Software Engineer", avatarImage: UIImage(named: "avatar_henry_brill")),
MSPersonaData(name: "Cecil Folk", subtitle: "Program Manager", avatarImage: UIImage(named: "avatar_cecil_folk"))
]
class MSPersonaListViewDemoController: DemoController {
override func viewDidLoad() {
super.viewDidLoad()
let personaListView = MSPersonaListView()
personaListView.personaList = personas()
personaListView.personaList = samplePersonas
personaListView.showsSearchDirectoryButton = true
personaListView.searchDirectoryDelegate = self
personaListView.onPersonaSelected = { [unowned self] persona in
@ -23,36 +51,6 @@ class MSPersonaListViewDemoController: DemoController {
view.addSubview(personaListView)
personaListView.fitIntoSuperview()
}
private func personas() -> [MSPersonaData] {
return [
MSPersonaData(name: "Kat Larrson", email: "kat.larrson@contoso.com", subtitle: "Designer", avatarImage: UIImage(named: "avatar_kat_larsson")),
MSPersonaData(name: "Kristin Patterson", email: "kristin.patterson@contoso.com", subtitle: "Software Engineer"),
MSPersonaData(name: "Ashley McCarthy", avatarImage: UIImage(named: "avatar_ashley_mccarthy")),
MSPersonaData(name: "Carole Poland", email: "carole.poland@contoso.com", subtitle: "Software Engineer"),
MSPersonaData(name: "Allan Munger", email: "allan.munger@contoso.com", subtitle: "Designer", avatarImage: UIImage(named: "avatar_allan_munger")),
MSPersonaData(name: "Amanda Brady", subtitle: "Program Manager", avatarImage: UIImage(named: "avatar_amanda_brady")),
MSPersonaData(name: "Kevin Sturgis", email: "kevin.sturgis@contoso.com", subtitle: "Software Engineeer"),
MSPersonaData(name: "Lydia Bauer", email: "lydia.bauer@contoso.com", avatarImage: UIImage(named: "avatar_lydia_bauer")),
MSPersonaData(name: "Robin Counts", subtitle: "Program Manager", avatarImage: UIImage(named: "avatar_robin_counts")),
MSPersonaData(name: "Tim Deboer", email: "tim.deboer@contoso.com", subtitle: "Designer", avatarImage: UIImage(named: "avatar_tim_deboer")),
MSPersonaData(email: "wanda.howard@contoso.com", subtitle: "Director"),
MSPersonaData(name: "Daisy Phillips", email: "daisy.phillips@contoso.com", subtitle: "Software Engineer", avatarImage: UIImage(named: "avatar_daisy_phillips")),
MSPersonaData(name: "Katri Ahokas", subtitle: "Program Manager", avatarImage: UIImage(named: "avatar_katri_ahokas")),
MSPersonaData(name: "Colin Ballinger", email: "colin.ballinger@contoso.com", subtitle: "Software Engineer", avatarImage: UIImage(named: "avatar_colin_ballinger")),
MSPersonaData(name: "Mona Kane", email: "mona.kane@contoso.com", subtitle: "Designer"),
MSPersonaData(name: "Elvia Atkins", email: "elvia.atkins@contoso.com", subtitle: "Software Engineer", avatarImage: UIImage(named: "avatar_elvia_atkins")),
MSPersonaData(name: "Johnie McConnell", subtitle: "Designer", avatarImage: UIImage(named: "avatar_johnie_mcconnell")),
MSPersonaData(name: "Charlotte Waltsson", email: "charlotte.waltsson@contoso.com", subtitle: "Software Engineer"),
MSPersonaData(name: "Mauricio August", email: "mauricio.august@contoso.com", subtitle: "Program Manager", avatarImage: UIImage(named: "avatar_mauricio_august")),
MSPersonaData(name: "Robert Tolbert", email: "robert.tolbert@contoso.com", subtitle: "Software Engineer", avatarImage: UIImage(named: "avatar_robert_tolbert")),
MSPersonaData(name: "Isaac Fielder", subtitle: "Designer", avatarImage: UIImage(named: "avatar_isaac_fielder")),
MSPersonaData(name: "Elliot Woodward", subtitle: "Designer"),
MSPersonaData(email: "carlos.slattery@contoso.com", subtitle: "Software Engineer"),
MSPersonaData(name: "Henry Brill", subtitle: "Software Engineer", avatarImage: UIImage(named: "avatar_henry_brill")),
MSPersonaData(name: "Cecil Folk", subtitle: "Program Manager", avatarImage: UIImage(named: "avatar_cecil_folk"))
]
}
}
// MARK: - MSPersonaListViewDemoController: MSPersonaListViewSearchDirectoryDelegate

Просмотреть файл

@ -0,0 +1,113 @@
//
// Copyright © 2018 Microsoft Corporation. All rights reserved.
//
import OfficeUIFabric
import UIKit
class MSPopupMenuDemoController: DemoController {
private enum CalendarLayout {
case agenda
case day
case threeDay
case week
case month
}
private var calendarLayout: CalendarLayout = .agenda
private var cityIndexPath: IndexPath? = IndexPath(item: 2, section: 1)
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Show", style: .plain, target: self, action: #selector(topBarButtonTapped))
toolbarItems = [
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
UIBarButtonItem(title: "Show with selection", style: .plain, target: self, action: #selector(bottomBarButtonTapped)),
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
]
navigationController?.isToolbarHidden = false
container.addArrangedSubview(createButton(title: "Show with sections", action: #selector(showTopMenuWithSectionsButtonTapped)))
container.addArrangedSubview(createButton(title: "Show with scrollable items and no icons", action: #selector(showTopMenuWithScrollableItemsButtonTapped)))
container.addArrangedSubview(UIView())
}
@objc private func topBarButtonTapped(sender: UIBarButtonItem) {
let controller = MSPopupMenuController(barButtonItem: sender, presentationDirection: .down)
controller.addItems([
MSPopupMenuItem(imageName: "mail-unread-25x25", title: "Unread"),
MSPopupMenuItem(imageName: "flag-25x25", title: "Flagged"),
MSPopupMenuItem(imageName: "attach-25x25", title: "Attachments")
])
let originalTitle = sender.title
sender.title = "Shown"
controller.onDismiss = {
sender.title = originalTitle
}
present(controller, animated: true)
}
@objc private func bottomBarButtonTapped(sender: UIBarButtonItem) {
var origin: CGFloat = -1
if let toolbar = navigationController?.toolbar {
origin = toolbar.convert(toolbar.bounds.origin, to: nil).y
}
let controller = MSPopupMenuController(barButtonItem: sender, presentationOrigin: origin, presentationDirection: .up)
controller.addItems([
MSPopupMenuItem(imageName: "agenda-25x25", title: "Agenda", isSelected: calendarLayout == .agenda, onSelected: { self.calendarLayout = .agenda }),
MSPopupMenuItem(imageName: "day-view-25x25", title: "Day", isSelected: calendarLayout == .day, onSelected: { self.calendarLayout = .day }),
MSPopupMenuItem(imageName: "week-view-25x25", title: "3-Day", isEnabled: false, isSelected: calendarLayout == .threeDay, onSelected: { self.calendarLayout = .threeDay }),
MSPopupMenuItem(title: "Week (no icon)", isSelected: calendarLayout == .week, onSelected: { self.calendarLayout = .week }),
MSPopupMenuItem(imageName: "month-view-25x25", title: "Month", isSelected: calendarLayout == .month, onSelected: { self.calendarLayout = .month })
])
let originalTitle = sender.title
sender.title = "Shown with selection"
controller.onDismiss = {
sender.title = originalTitle
}
present(controller, animated: true)
}
@objc private func showTopMenuWithSectionsButtonTapped(sender: UIButton) {
let controller = MSPopupMenuController(sourceView: sender, sourceRect: sender.bounds, presentationDirection: .down)
controller.addSections([
MSPopupMenuSection(title: "Canada", items: [
MSPopupMenuItem(imageName: "Montreal", generateSelectedImage: false, title: "Montréal", subtitle: "Québec"),
MSPopupMenuItem(imageName: "Toronto", generateSelectedImage: false, title: "Toronto", subtitle: "Ontario"),
MSPopupMenuItem(imageName: "Vancouver", generateSelectedImage: false, title: "Vancouver", subtitle: "British Columbia")
]),
MSPopupMenuSection(title: "United States", items: [
MSPopupMenuItem(imageName: "Las Vegas", generateSelectedImage: false, title: "Las Vegas", subtitle: "Nevada"),
MSPopupMenuItem(imageName: "Phoenix", generateSelectedImage: false, title: "Phoenix", subtitle: "Arizona"),
MSPopupMenuItem(imageName: "San Francisco", generateSelectedImage: false, title: "San Francisco", subtitle: "California"),
MSPopupMenuItem(imageName: "Seattle", generateSelectedImage: false, title: "Seattle", subtitle: "Washington")
])
])
controller.selectedItemIndexPath = cityIndexPath
controller.onDismiss = { [unowned controller] in
self.cityIndexPath = controller.selectedItemIndexPath
}
present(controller, animated: true)
}
@objc private func showTopMenuWithScrollableItemsButtonTapped(sender: UIButton) {
let controller = MSPopupMenuController(sourceView: sender, sourceRect: sender.bounds, presentationDirection: .down)
let items = samplePersonas.map { MSPopupMenuItem(title: !$0.name.isEmpty ? $0.name : $0.email) }
controller.addItems(items)
present(controller, animated: true)
}
}

Просмотреть файл

@ -13,6 +13,12 @@
A559BB7F212B6FA40055E107 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = A559BB81212B6FA40055E107 /* Localizable.strings */; };
A559BB83212B7D870055E107 /* OfficeUIFabric.swift in Sources */ = {isa = PBXBuildFile; fileRef = A559BB82212B7D870055E107 /* OfficeUIFabric.swift */; };
A589F854211BA03200471C23 /* MSLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A589F853211BA03200471C23 /* MSLabel.swift */; };
A5961F9D218A254D00E2A506 /* MSPopupMenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5961F9C218A254D00E2A506 /* MSPopupMenuController.swift */; };
A5961F9F218A256B00E2A506 /* MSPopupMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5961F9E218A256B00E2A506 /* MSPopupMenuItem.swift */; };
A5961FA1218A25C400E2A506 /* MSPopupMenuSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5961FA0218A25C400E2A506 /* MSPopupMenuSection.swift */; };
A5961FA3218A25D100E2A506 /* MSPopupMenuItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5961FA2218A25D100E2A506 /* MSPopupMenuItemCell.swift */; };
A5961FA5218A260500E2A506 /* MSPopupMenuSectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5961FA4218A260500E2A506 /* MSPopupMenuSectionHeaderView.swift */; };
A5961FA7218A2E4500E2A506 /* UIImage+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5961FA6218A2E4500E2A506 /* UIImage+Extensions.swift */; };
A5B87AEF211BCA710038C37C /* UIFontDescriptor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B87AEE211BCA710038C37C /* UIFontDescriptor+Extension.swift */; };
A5B87AF1211BD4380038C37C /* UIFont+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B87AF0211BD4380038C37C /* UIFont+Extension.swift */; };
A5B87AF6211E16370038C37C /* MSDrawerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B87AF3211E16360038C37C /* MSDrawerController.swift */; };
@ -90,6 +96,12 @@
A559BB80212B6FA40055E107 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
A559BB82212B7D870055E107 /* OfficeUIFabric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfficeUIFabric.swift; sourceTree = "<group>"; };
A589F853211BA03200471C23 /* MSLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSLabel.swift; sourceTree = "<group>"; };
A5961F9C218A254D00E2A506 /* MSPopupMenuController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSPopupMenuController.swift; sourceTree = "<group>"; };
A5961F9E218A256B00E2A506 /* MSPopupMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSPopupMenuItem.swift; sourceTree = "<group>"; };
A5961FA0218A25C400E2A506 /* MSPopupMenuSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSPopupMenuSection.swift; sourceTree = "<group>"; };
A5961FA2218A25D100E2A506 /* MSPopupMenuItemCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSPopupMenuItemCell.swift; sourceTree = "<group>"; };
A5961FA4218A260500E2A506 /* MSPopupMenuSectionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MSPopupMenuSectionHeaderView.swift; sourceTree = "<group>"; };
A5961FA6218A2E4500E2A506 /* UIImage+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Extensions.swift"; sourceTree = "<group>"; };
A5B87AEE211BCA710038C37C /* UIFontDescriptor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFontDescriptor+Extension.swift"; sourceTree = "<group>"; };
A5B87AF0211BD4380038C37C /* UIFont+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Extension.swift"; sourceTree = "<group>"; };
A5B87AF3211E16360038C37C /* MSDrawerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MSDrawerController.swift; sourceTree = "<group>"; };
@ -172,6 +184,18 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
A5961F9B218A251E00E2A506 /* Popup Menu */ = {
isa = PBXGroup;
children = (
A5961F9C218A254D00E2A506 /* MSPopupMenuController.swift */,
A5961F9E218A256B00E2A506 /* MSPopupMenuItem.swift */,
A5961FA0218A25C400E2A506 /* MSPopupMenuSection.swift */,
A5961FA2218A25D100E2A506 /* MSPopupMenuItemCell.swift */,
A5961FA4218A260500E2A506 /* MSPopupMenuSectionHeaderView.swift */,
);
path = "Popup Menu";
sourceTree = "<group>";
};
A5B87AED211BCA4A0038C37C /* Extensions */ = {
isa = PBXGroup;
children = (
@ -185,6 +209,7 @@
A5B87AFF211E1B700038C37C /* UIDevice+Extension.swift */,
A5B87AF0211BD4380038C37C /* UIFont+Extension.swift */,
A5B87AEE211BCA710038C37C /* UIFontDescriptor+Extension.swift */,
A5961FA6218A2E4500E2A506 /* UIImage+Extensions.swift */,
FD5BBE3E214C68F8008964B4 /* UINavigationBar+Extensions.swift */,
A5B87B01211E20B50038C37C /* UIScreen+Extension.swift */,
B444D6B02181403C0002B4D4 /* UITableViewCell+Extension.swift */,
@ -232,6 +257,7 @@
A5B87AF2211E13D00038C37C /* Drawer */,
A5B87AED211BCA4A0038C37C /* Extensions */,
B426613C214731AC00E25423 /* People Picker */,
A5961F9B218A251E00E2A506 /* Popup Menu */,
FD7254EE2147382D002F4069 /* Presenters */,
B444D6B421825B510002B4D4 /* Table View */,
A5CEC17020D996120016922A /* Resources */,
@ -512,10 +538,12 @@
buildActionMask = 2147483647;
files = (
B46D3F91215056940029772C /* CharacterSet+Extension.swift in Sources */,
A5961FA3218A25D100E2A506 /* MSPopupMenuItemCell.swift in Sources */,
FD599D0221348439008845EE /* MSCalendarView.swift in Sources */,
A559BB83212B7D870055E107 /* OfficeUIFabric.swift in Sources */,
FD599D082134AB0E008845EE /* MSCalendarViewWeekdayHeadingView.swift in Sources */,
FDFB8AF321361C9D0046850A /* MSCalendarViewDayMonthYearCell.swift in Sources */,
A5961F9D218A254D00E2A506 /* MSPopupMenuController.swift in Sources */,
A5B87B00211E1B700038C37C /* UIDevice+Extension.swift in Sources */,
B4E782C521793BB900A7DFCE /* MSActivityIndicatorView.swift in Sources */,
FD5BBE41214C6AF3008964B4 /* MSTwoLinesTitleView.swift in Sources */,
@ -533,6 +561,7 @@
B4EF53C3215AF1AB00573E8F /* MSPersona.swift in Sources */,
FDFB8AEB21361C950046850A /* MSCalendarViewMonthBannerView.swift in Sources */,
A5B87B06211E23650038C37C /* UIView+Extensions.swift in Sources */,
A5961FA7218A2E4500E2A506 /* UIImage+Extensions.swift in Sources */,
B42661422148568800E25423 /* MSAvatarView.swift in Sources */,
FD7254E72146E946002F4069 /* MSCalendarViewDayCell.swift in Sources */,
A589F854211BA03200471C23 /* MSLabel.swift in Sources */,
@ -548,6 +577,7 @@
A5CEC16F20D98F340016922A /* Fonts.swift in Sources */,
FD599D0C2134AB1E008845EE /* MSCalendarViewDataSource.swift in Sources */,
A5B87B04211E22B70038C37C /* MSDimmingView.swift in Sources */,
A5961F9F218A256B00E2A506 /* MSPopupMenuItem.swift in Sources */,
B4E782C321793AB200A7DFCE /* MSActivityIndicatorCell.swift in Sources */,
FD4F2A1E214ADBCF00C437D6 /* MSDatePicker.swift in Sources */,
FD599D0A2134AB15008845EE /* MSCalendarViewLayout.swift in Sources */,
@ -556,6 +586,7 @@
A5CEC23120E451D00016922A /* MSButton.swift in Sources */,
FDF41EDB2141A23B00EC527C /* UIViewController+Extensions.swift in Sources */,
FDA1AF8C21484625001AE720 /* MSBlurringView.swift in Sources */,
A5961FA1218A25C400E2A506 /* MSPopupMenuSection.swift in Sources */,
A5B87AF6211E16370038C37C /* MSDrawerController.swift in Sources */,
A5CEC16D20D98EE70016922A /* Colors.swift in Sources */,
FD0D29D62151A3D700E8655E /* MSCardPresenterNavigationController.swift in Sources */,
@ -565,6 +596,7 @@
FDF41ED92141A02200EC527C /* MSCalendarConfiguration.swift in Sources */,
FD7254E92147059D002F4069 /* Calendar+Extensions.swift in Sources */,
A559BB7E212B6D100055E107 /* String+Extension.swift in Sources */,
A5961FA5218A260500E2A506 /* MSPopupMenuSectionHeaderView.swift in Sources */,
B46D3F9D215985AC0029772C /* MSPersonaListView.swift in Sources */,
A5DCA76421224026005F4CB7 /* MSSeparator.swift in Sources */,
FD599D062134A682008845EE /* AccessibleViewDelegate.swift in Sources */,

Просмотреть файл

@ -2,6 +2,8 @@
// Copyright © 2018 Microsoft Corporation. All rights reserved.
//
// MARK: MSColors
public struct MSColors {
// MARK: Primary
@ -69,25 +71,22 @@ public struct MSColors {
// TODO: Add semantic colors describing colors used for particular control elements (must reference physical colors)
public static let activityIndicator: UIColor = gray
public static let background: UIColor = white
public static let centeredLabelText: UIColor = primary
public static let separator: UIColor = borderLightGray
public struct Action {
public static let text: UIColor = primary
public static let textHighlighted: UIColor = primary.withAlphaComponent(0.4)
public static let textDestructive: UIColor = warning
public static let textDestructiveHighlighted: UIColor = warning.withAlphaComponent(0.4)
}
public struct Avatar {
public static let text: UIColor = white
}
public struct Persona {
public static let name: UIColor = black
public static let subtitle: UIColor = gray
public static let background: UIColor = white
public static let backgroundSelected: UIColor = backgroundGray
}
public static let activityIndicator: UIColor = gray
public static let background: UIColor = white
public static let centeredLabelText: UIColor = primary
public static let separator: UIColor = borderLightGray
public struct CalendarView {
public struct TodayCell {
public static let background: UIColor = white
@ -105,12 +104,35 @@ public struct MSColors {
public static let selectedCircleNormalColor: UIColor = primary
}
}
public struct PageCardPresenter {
public static let currentPageIndicatorTintColor: UIColor = white
public static let pageIndicatorTintColor: UIColor = white.withAlphaComponent(0.5)
}
public struct Persona {
public static let name: UIColor = black
public static let subtitle: UIColor = gray
public static let background: UIColor = white
public static let backgroundSelected: UIColor = backgroundGray
}
public struct PopupMenu {
public struct Item {
public static let imageSelected: UIColor = primary
public static let title: UIColor = black
public static let titleSelected: UIColor = primary
public static let titleDisabled: UIColor = borderLightGray
public static let subtitle: UIColor = gray
public static let subtitleSelected: UIColor = primary
public static let subtitleDisabled: UIColor = borderLightGray
}
public static let sectionHeader: UIColor = darkGray
}
}
// MARK: - MSTextColorStyle
public enum MSTextColorStyle: Int {
case regular
case secondary

Просмотреть файл

@ -35,7 +35,7 @@ open class MSDrawerController: UIViewController {
Transition is always animated when drawer is visible.
*/
open var isExpanded: Bool = false {
@objc open var isExpanded: Bool = false {
didSet {
if isExpanded == oldValue {
return
@ -82,6 +82,11 @@ open class MSDrawerController: UIViewController {
}
}
/// `onDismiss` is called when popup menu is being dismissed.
@objc open var onDismiss: (() -> Void)?
/// `onDismissCompleted` is called after popup menu was dismissed.
@objc open var onDismissCompleted: (() -> Void)?
private let sourceView: UIView?
private let sourceRect: CGRect?
private let barButtonItem: UIBarButtonItem?
@ -135,7 +140,7 @@ open class MSDrawerController: UIViewController {
fatalError("init(coder:) has not been implemented")
}
private func initialize() {
open func initialize() {
modalPresentationStyle = .custom
transitioningDelegate = self
}
@ -146,6 +151,20 @@ open class MSDrawerController: UIViewController {
view.isAccessibilityElement = false
}
open override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if isBeingDismissed {
onDismiss?()
}
}
open override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if isBeingDismissed {
onDismissCompleted?()
}
}
open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
if !isBeingPresented && presentationController is MSDrawerPresentationController {
@ -161,13 +180,13 @@ open class MSDrawerController: UIViewController {
private func presentationStyle(for sourceViewController: UIViewController) -> PresentationStyle {
guard let window = sourceViewController.view?.window else {
// No window, use the device type as last resort. It will be only a problem in splitView on iPad
// No window, use the device type as last resort.
// It will be a problem:
// - on iPhone Plus/X in landscape orientation
// - on iPad in split view
return UIDevice.isPhone ? .slideover : .popover
}
if window.traitCollection.horizontalSizeClass == .compact || UIDevice.isPhonePlus {
return .slideover
}
return .popover
return window.traitCollection.horizontalSizeClass == .compact ? .slideover : .popover
}
}
@ -175,7 +194,7 @@ open class MSDrawerController: UIViewController {
extension MSDrawerController: UIViewControllerTransitioningDelegate {
public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if presented.presentationController is MSDrawerPresentationController {
if presentationStyle(for: source) == .slideover {
return MSDrawerTransitionAnimator(presenting: true, presentationDirection: presentationDirection)
}
return nil
@ -196,6 +215,7 @@ extension MSDrawerController: UIViewControllerTransitioningDelegate {
let presentationController = UIPopoverPresentationController(presentedViewController: presented, presenting: presenting)
presentationController.backgroundColor = MSColors.background
presentationController.permittedArrowDirections = permittedArrowDirections
presentationController.delegate = self
if let sourceView = sourceView {
presentationController.sourceView = sourceView
@ -212,3 +232,11 @@ extension MSDrawerController: UIViewControllerTransitioningDelegate {
}
}
}
// MARK: - MSDrawerController: UIPopoverPresentationControllerDelegate
extension MSDrawerController: UIPopoverPresentationControllerDelegate {
public func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
return .none
}
}

Просмотреть файл

@ -6,12 +6,14 @@
class MSDrawerTransitionAnimator: NSObject {
private struct Constants {
static let animationDurationMin: TimeInterval = 0.15
static let animationDurationMax: TimeInterval = 0.25
static let animationSpeed: CGFloat = 1300 // pixels per second
}
static func animationDuration(forSizeChange change: CGFloat) -> TimeInterval {
return min(TimeInterval(abs(change) / Constants.animationSpeed), Constants.animationDurationMax)
let duration = TimeInterval(abs(change) / Constants.animationSpeed)
return max(Constants.animationDurationMin, min(duration, Constants.animationDurationMax))
}
let presenting: Bool

Просмотреть файл

@ -3,6 +3,8 @@
//
public extension UIFont {
var deviceLineHeight: CGFloat { return UIScreen.main.roundToDevicePixels(lineHeight) }
func withWeight(_ weight: UIFont.Weight) -> UIFont {
return UIFont(descriptor: fontDescriptor.withWeight(weight), size: pointSize)
}

Просмотреть файл

@ -0,0 +1,51 @@
//
// Copyright © 2018 Microsoft Corporation. All rights reserved.
//
public extension UIImage {
/// Full replacement for `UIImage(named:)` which attempts to recolor image assets with a color from the high contrast
/// color palette when `Darken Colors` is enabled. The method is called `static` because the images are outputted with
/// the rendering mode `AlwaysOriginal` with the intention of preventing further recoloring by `tintColor`.
@objc class func staticImageNamed(_ name: String, in bundle: Bundle? = nil, withPrimaryColorForDarkerSystemColors primaryColor: UIColor? = nil) -> UIImage? {
guard var image = UIImage(named: name, in: bundle, compatibleWith: nil) else {
NSLog("Missing image asset with name: \(name)")
return nil
}
if UIAccessibilityDarkerSystemColorsEnabled(), let primaryColor = primaryColor {
// Recolor image with high contrast version of `primaryColor`
image = recolorImage(image, withPrimaryColor: primaryColor)
}
// Force image to be `AlwaysOriginal` regardless of the setting in `.xcassets` to prevent recoloring caused by `tintColor`
return image.withRenderingMode(.alwaysOriginal)
}
internal class func staticImageNamed(_ name: String) -> UIImage? {
// TODO: Provide primary color for known images
return staticImageNamed(name, in: OfficeUIFabricFramework.bundle)
}
private class func recolorImage(_ originalImage: UIImage, withPrimaryColor primaryColor: UIColor) -> UIImage {
UIGraphicsBeginImageContextWithOptions(originalImage.size, false, originalImage.scale)
// Fill canvas with `primaryColor`
primaryColor.setFill()
UIRectFill(CGRect(x: 0.0, y: 0.0, width: originalImage.size.width, height: originalImage.size.height))
// Apply the `alpha` component of the image to the canvas
originalImage.draw(at: CGPoint.zero, blendMode: .destinationIn, alpha: 1.0)
// Create `image`
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
@objc public func image(withPrimaryColor primaryColor: UIColor) -> UIImage {
// Force image to be `AlwaysOriginal` regardless of the setting in `.xcassets` to prevent recoloring caused by `tintColor`
return UIImage.recolorImage(self, withPrimaryColor: primaryColor).withRenderingMode(.alwaysOriginal)
}
}

Просмотреть файл

@ -0,0 +1,242 @@
//
// Copyright © 2018 Microsoft Corporation. All rights reserved.
//
/**
`MSPopupMenuController` is used to present a popup menu that slides from top or bottom depending on `presentationDirection`. Use `presentationOrigin` to specify the vertical offset (in screen coordinates) from which to show popup menu. If not provided it will be calculated automatically: bottom of navigation bar for `.down` presentation and bottom of the screen for `.up` presentation.
`MSPopupMenuController` will be presented as a popover on iPad and so requires either `sourceView`/`sourceRect` or `barButtonItem` to be provided via available initializers. Use `permittedArrowDirections` to specify the direction of the popover arrow.
*/
open class MSPopupMenuController: MSDrawerController {
private struct Constants {
static let minimumWidthForPopover: CGFloat = 250
}
open override var preferredContentSize: CGSize {
get { return CGSize(width: preferredWidth, height: preferredHeight) }
set { }
}
open override var preferredWidth: CGFloat {
guard presentationController is UIPopoverPresentationController else {
return super.preferredWidth
}
var width = Constants.minimumWidthForPopover
for section in sections {
width = max(width, MSPopupMenuSectionHeaderView.preferredWidth(for: section))
for item in section.items {
width = max(width, MSPopupMenuItemCell.preferredWidth(for: item, preservingSpaceForImage: itemsHaveImages))
}
}
return width
}
open var preferredHeight: CGFloat {
var height: CGFloat = 0
for section in sections {
height += MSPopupMenuSectionHeaderView.preferredHeight(for: section)
for item in section.items {
height += MSPopupMenuItemCell.preferredHeight(for: item)
}
}
return height
}
/// Use `selectedItemIndexPath` to get or set the selected menu item instead of doing this via `MSPopupMenuItem` directly
@objc open var selectedItemIndexPath: IndexPath? {
get {
for (sectionIndex, section) in sections.enumerated() {
for (itemIndex, item) in section.items.enumerated() {
if item.isSelected {
return IndexPath(item: itemIndex, section: sectionIndex)
}
}
}
return nil
}
set {
for (sectionIndex, section) in sections.enumerated() {
for (itemIndex, item) in section.items.enumerated() {
item.isSelected = sectionIndex == newValue?.section && itemIndex == newValue?.item
}
}
}
}
private var sections: [MSPopupMenuSection] = []
private var tableView = UITableView()
private var itemsHaveImages: Bool {
return sections.contains(where: { $0.items.contains(where: { $0.image != nil }) })
}
private var needsScrolling: Bool {
return tableView.contentSize.height > tableView.bounds.height
}
/// Append new items to the last section of the menu
/// - note: If there is no section in the menu, create a new one without header and append the items to it
@objc public func addItems(_ items: [MSPopupMenuItem]) {
if let section = sections.last {
section.items.append(contentsOf: items)
} else {
let section = MSPopupMenuSection(title: nil, items: items)
sections.append(section)
}
}
/// Append a new section to the end of menu
@objc public func addSection(_ section: MSPopupMenuSection) {
sections.append(section)
}
/// Append new sections to the end of menu
@objc public func addSections(_ sections: [MSPopupMenuSection]) {
self.sections.append(contentsOf: sections)
}
open override func initialize() {
super.initialize()
tableView.backgroundColor = .clear
tableView.separatorStyle = .none
// Prevent automatic insetting of this non-scrollable content
if #available(iOS 11.0, *) {
tableView.contentInsetAdjustmentBehavior = .never
}
tableView.isAccessibilityElement = true
// Prevent tap delay when selecting a menu item
tableView.delaysContentTouches = false
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture))
panGestureRecognizer.delegate = self
tableView.addGestureRecognizer(panGestureRecognizer)
}
open override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
tableView.fitIntoSuperview()
tableView.register(MSPopupMenuItemCell.self, forCellReuseIdentifier: MSPopupMenuItemCell.identifier)
tableView.register(MSPopupMenuSectionHeaderView.self, forHeaderFooterViewReuseIdentifier: MSPopupMenuSectionHeaderView.identifier)
tableView.delegate = self
tableView.dataSource = self
}
open override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tableView.selectRow(at: selectedItemIndexPath, animated: false, scrollPosition: .none)
}
@objc private func handlePanGesture(gestureRecognizer: UIGestureRecognizer) {
let point = gestureRecognizer.location(in: tableView)
switch gestureRecognizer.state {
case .began, .changed:
// Warm up all the visible cell feedback generators
// Each cell has its own generator to allow each to fire perfectly on time when highlight changes
for case let cell as MSPopupMenuItemCell in tableView.visibleCells {
if cell.feedbackGenerator == nil {
cell.feedbackGenerator = UISelectionFeedbackGenerator()
}
cell.feedbackGenerator?.prepare()
}
var cell: UITableViewCell?
if let indexPath = tableView.indexPathForRow(at: point) {
cell = tableView.cellForRow(at: indexPath)
}
for visibleCell in tableView.visibleCells {
visibleCell.isHighlighted = visibleCell == cell
}
case .ended, .cancelled, .failed:
guard let indexPath = tableView.indexPathForRow(at: point) else {
// Gesture did not finish on a highlighted cell, dismiss the dropdown
presentingViewController?.dismiss(animated: true)
return
}
let item = sections[indexPath.section].items[indexPath.row]
if !item.isEnabled {
return
}
tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
tableView(tableView, didSelectRowAt: indexPath)
default:
return
}
}
}
// MARK: - MSPopupMenuController: UITableViewDataSource
extension MSPopupMenuController: UITableViewDataSource {
public func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sections[section].items.count
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let section = indexPath.section
let row = indexPath.row
let item = sections[section].items[row]
let cell = tableView.dequeueReusableCell(withIdentifier: MSPopupMenuItemCell.identifier) as! MSPopupMenuItemCell
cell.setup(item: item)
cell.preservesSpaceForImage = itemsHaveImages
cell.showsSeparator = section != tableView.numberOfSections - 1 || row != tableView.numberOfRows(inSection: section) - 1
return cell
}
}
// MARK: - MSPopupMenuController: UITableViewDelegate
extension MSPopupMenuController: UITableViewDelegate {
public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return MSPopupMenuSectionHeaderView.preferredHeight(for: sections[section])
}
public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let item = sections[indexPath.section].items[indexPath.row]
return MSPopupMenuItemCell.preferredHeight(for: item)
}
public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let section = sections[section]
if !MSPopupMenuSectionHeaderView.isHeaderVisible(for: section) {
return nil
}
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: MSPopupMenuSectionHeaderView.identifier) as! MSPopupMenuSectionHeaderView
headerView.setup(section: section)
return headerView
}
public func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
let item = sections[indexPath.section].items[indexPath.row]
return item.isEnabled ? indexPath : nil
}
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
selectedItemIndexPath = indexPath
sections[indexPath.section].items[indexPath.row].onSelected?()
if !isBeingDismissed {
presentingViewController?.dismiss(animated: true)
}
}
}
// MARK: - MSPopupMenuController: UIGestureRecognizerDelegate
extension MSPopupMenuController: UIGestureRecognizerDelegate {
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return !needsScrolling
}
}

Просмотреть файл

@ -0,0 +1,36 @@
//
// Copyright © 2018 Microsoft Corporation. All rights reserved.
//
/**
`MSPopupMenuItem` represents a menu item inside `MSPopupMenuController`.
*/
@objcMembers
open class MSPopupMenuItem: NSObject {
public let image: UIImage?
public let selectedImage: UIImage?
public let title: String
public let subtitle: String?
public var isEnabled: Bool = true
public var isSelected: Bool = false
public let onSelected: (() -> Void)?
public init(image: UIImage? = nil, selectedImage: UIImage? = nil, title: String, subtitle: String? = nil, isEnabled: Bool = true, isSelected: Bool = false, onSelected: (() -> Void)? = nil) {
self.image = image
self.selectedImage = selectedImage ?? image
self.title = title
self.subtitle = subtitle
self.isEnabled = isEnabled
self.isSelected = isSelected
self.onSelected = onSelected
super.init()
}
public convenience init(imageName: String, generateSelectedImage: Bool = true, title: String, subtitle: String? = nil, isEnabled: Bool = true, isSelected: Bool = false, onSelected: (() -> Void)? = nil) {
let image = UIImage.staticImageNamed(imageName, in: nil)
let selectedImage = generateSelectedImage ? image?.image(withPrimaryColor: MSColors.PopupMenu.Item.imageSelected) : nil
self.init(image: image, selectedImage: selectedImage, title: title, subtitle: subtitle, isEnabled: isEnabled, isSelected: isSelected, onSelected: onSelected)
}
}

Просмотреть файл

@ -0,0 +1,242 @@
//
// Copyright © 2018 Microsoft Corporation. All rights reserved.
//
class MSPopupMenuItemCell: UITableViewCell {
private struct Constants {
static let oneLineHeight: CGFloat = 50.0
static let twoLineHeight: CGFloat = 62.0
static let horizontalSpacing: CGFloat = 16.0
static let verticalSpacing: CGFloat = 2.0
static let imageViewSize: CGFloat = 25.0
static let selectedImageViewSize: CGFloat = 20.0
static let titleFontStyle: MSTextStyle = .body
static let subtitleFontStyle: MSTextStyle = .footnote
static let defaultAlpha: CGFloat = 1.0
static let highlightedAlpha: CGFloat = 0.4
static let animationDuration: TimeInterval = 0.15
}
static let identifier: String = "MSPopupMenuItemCell"
static func preferredWidth(for item: MSPopupMenuItem, preservingSpaceForImage preserveSpaceForImage: Bool) -> CGFloat {
let labelFont = Constants.titleFontStyle.font
let labelSize = item.title.boundingRect(
with: CGSize(width: .greatestFiniteMagnitude, height: labelFont.deviceLineHeight),
options: [.usesLineFragmentOrigin, .usesFontLeading],
attributes: [.font: labelFont],
context: nil
)
let spacing = Constants.horizontalSpacing
var width = spacing + labelSize.width + spacing + Constants.selectedImageViewSize + spacing
if item.image != nil || preserveSpaceForImage {
width += Constants.imageViewSize + spacing
}
return width
}
static func preferredHeight(for item: MSPopupMenuItem) -> CGFloat {
return item.subtitle == nil ? Constants.oneLineHeight : Constants.twoLineHeight
}
var feedbackGenerator: UISelectionFeedbackGenerator?
var preservesSpaceForImage: Bool = false
var showsSeparator: Bool = true {
didSet {
separator.isHidden = !showsSeparator
}
}
private var item: MSPopupMenuItem?
// Cannot use imageView since it exists in superclass
private let _imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .center
return imageView
}()
private let titleLabel: UILabel = {
let label = UILabel()
label.font = Constants.titleFontStyle.font
label.lineBreakMode = .byTruncatingTail
return label
}()
private let subtitleLabel: UILabel = {
let label = UILabel()
label.font = Constants.subtitleFontStyle.font
label.lineBreakMode = .byTruncatingTail
return label
}()
private let selectedImageView: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage.staticImageNamed("checkmark-blue-20x20")
imageView.contentMode = .center
return imageView
}()
private let separator = MSSeparator()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
backgroundColor = .clear
selectionStyle = .none
addSubview(_imageView)
addSubview(titleLabel)
addSubview(subtitleLabel)
addSubview(selectedImageView)
addSubview(separator)
isAccessibilityElement = true
accessibilityTraits = UIAccessibilityTraitButton
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup(item: MSPopupMenuItem) {
self.item = item
_imageView.image = item.image
_imageView.highlightedImage = item.selectedImage
_imageView.isHidden = _imageView.image == nil
titleLabel.text = item.title
subtitleLabel.text = item.subtitle
updateViews()
var accessibilityString = "\(item.title)"
if let subtitle = item.subtitle {
accessibilityString.append(", \(subtitle)")
}
accessibilityLabel = accessibilityString
if item.isEnabled {
accessibilityTraits &= ~UIAccessibilityTraitNotEnabled
} else {
accessibilityTraits |= UIAccessibilityTraitNotEnabled
}
}
override func layoutSubviews() {
super.layoutSubviews()
var leftOffset: CGFloat = safeAreaInsetsIfAvailable.left + Constants.horizontalSpacing
let rightOffset = bounds.width - safeAreaInsetsIfAvailable.right
if !_imageView.isHidden || preservesSpaceForImage {
_imageView.frame = CGRect(
x: leftOffset,
y: UIScreen.main.middleOrigin(bounds.height, containedSizeValue: Constants.imageViewSize),
width: Constants.imageViewSize,
height: Constants.imageViewSize
)
leftOffset += _imageView.width + Constants.horizontalSpacing
}
selectedImageView.frame = CGRect(
x: rightOffset - Constants.horizontalSpacing - Constants.selectedImageViewSize,
y: UIScreen.main.middleOrigin(height, containedSizeValue: Constants.selectedImageViewSize),
width: Constants.selectedImageViewSize,
height: Constants.selectedImageViewSize
)
separator.frame = CGRect(x: leftOffset, y: bounds.height - separator.height, width: rightOffset - leftOffset, height: separator.height)
var labelWidth = rightOffset - Constants.horizontalSpacing - leftOffset
if !selectedImageView.isHidden {
labelWidth -= Constants.horizontalSpacing + selectedImageView.width
}
let titleLabelHeight = titleLabel.font.deviceLineHeight
let subtitleLabelHeight = subtitleLabel.font.deviceLineHeight
let isSubtitleVisible = subtitleLabel.text != nil
var labelAreaHeight = titleLabelHeight
if isSubtitleVisible {
labelAreaHeight += Constants.verticalSpacing + subtitleLabelHeight
}
var labelTop = UIScreen.main.middleOrigin(bounds.height, containedSizeValue: labelAreaHeight)
titleLabel.frame = CGRect(x: leftOffset, y: labelTop, width: labelWidth, height: titleLabelHeight)
if isSubtitleVisible {
labelTop += titleLabel.height + Constants.verticalSpacing
subtitleLabel.frame = CGRect(x: leftOffset, y: labelTop, width: labelWidth, height: subtitleLabelHeight)
}
}
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
let oldValue = isHighlighted
super.setHighlighted(highlighted, animated: animated)
if isHighlighted == oldValue {
return
}
// Override default background color change
backgroundColor = .clear
// Give feedback if needed
if highlighted {
feedbackGenerator?.selectionChanged()
feedbackGenerator?.prepare()
}
if animated {
UIView.animate(withDuration: Constants.animationDuration) {
self.updateViews()
}
} else {
updateViews()
}
}
override func setSelected(_ selected: Bool, animated: Bool) {
let oldValue = isSelected
super.setSelected(selected, animated: animated)
if isSelected == oldValue {
return
}
// Override default background color change
backgroundColor = .clear
if animated {
UIView.animate(withDuration: Constants.animationDuration) {
self.updateViews()
}
} else {
updateViews()
}
}
private func updateViews() {
if item?.isEnabled == false {
titleLabel.textColor = MSColors.PopupMenu.Item.titleDisabled
subtitleLabel.textColor = MSColors.PopupMenu.Item.subtitleDisabled
} else {
// Highlight
let alpha = isHighlighted ? Constants.highlightedAlpha : Constants.defaultAlpha
_imageView.alpha = alpha
titleLabel.alpha = alpha
subtitleLabel.alpha = alpha
// Selection
_imageView.isHighlighted = isSelected
titleLabel.textColor = isSelected ? MSColors.PopupMenu.Item.titleSelected : MSColors.PopupMenu.Item.title
subtitleLabel.textColor = isSelected ? MSColors.PopupMenu.Item.subtitleSelected : MSColors.PopupMenu.Item.subtitle
}
selectedImageView.isHidden = !isSelected
}
}

Просмотреть файл

@ -0,0 +1,18 @@
//
// Copyright © 2018 Microsoft Corporation. All rights reserved.
//
/**
`MSPopupMenuSection` represents a section of menu items inside `MSPopupMenuController`.
*/
@objcMembers
open class MSPopupMenuSection: NSObject {
public let title: String?
public var items: [MSPopupMenuItem]
public init(title: String?, items: [MSPopupMenuItem]) {
self.title = title
self.items = items
super.init()
}
}

Просмотреть файл

@ -0,0 +1,71 @@
//
// Copyright © 2018 Microsoft Corporation. All rights reserved.
//
class MSPopupMenuSectionHeaderView: UITableViewHeaderFooterView {
private struct Constants {
static let padding = UIEdgeInsets(top: 14, left: 16, bottom: 0, right: 16)
static let titleFontStyle: MSTextStyle = .subhead
}
static let identifier: String = "MSPopupMenuSectionHeaderView"
static func isHeaderVisible(for section: MSPopupMenuSection) -> Bool {
return section.title != nil
}
static func preferredWidth(for section: MSPopupMenuSection) -> CGFloat {
guard isHeaderVisible(for: section), let title = section.title else {
return 0
}
let labelFont = Constants.titleFontStyle.font
let labelSize = title.boundingRect(
with: CGSize(width: .greatestFiniteMagnitude, height: labelFont.deviceLineHeight),
options: [.usesLineFragmentOrigin, .usesFontLeading],
attributes: [.font: labelFont],
context: nil
)
return Constants.padding.left + labelSize.width + Constants.padding.right
}
static func preferredHeight(for section: MSPopupMenuSection) -> CGFloat {
if isHeaderVisible(for: section) {
return Constants.padding.top + Constants.titleFontStyle.font.deviceLineHeight + Constants.padding.bottom
} else {
return 0
}
}
private let label: UILabel = {
let label = UILabel()
label.font = Constants.titleFontStyle.font
label.textColor = MSColors.PopupMenu.sectionHeader
return label
}()
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
let view = UIView(frame: .zero)
view.backgroundColor = .clear
backgroundView = view
contentView.addSubview(label)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup(section: MSPopupMenuSection) {
label.text = section.title
}
override func layoutSubviews() {
super.layoutSubviews()
let labelSize = label.sizeThatFits(bounds.size)
label.frame = CGRect(origin: CGPoint(x: Constants.padding.left, y: Constants.padding.top), size: labelSize)
}
}

Просмотреть файл

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "checkmark-20x20.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Двоичные данные
OfficeUIFabric/Resources/Assets.xcassets/checkmark-blue-20x20.imageset/checkmark-20x20.pdf поставляемый Normal file

Двоичный файл не отображается.