Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9a48e40
add UKModalController, UKBottomModalController and UKCenterModalContr…
mikhailChelbaev Nov 22, 2024
3962820
regroup files
mikhailChelbaev Nov 22, 2024
9473d95
add base properties to ModalVM
mikhailChelbaev Nov 22, 2024
bbcc1e0
change type of the container to scroll view to avoid truncating conte…
mikhailChelbaev Nov 22, 2024
e8dcf27
fix mistake in naming
mikhailChelbaev Nov 25, 2024
bedfb0a
split modal content into header, body and footer
mikhailChelbaev Nov 25, 2024
838eee7
add `modalPaddings` property to the vm
mikhailChelbaev Nov 25, 2024
32160b1
add modal size property
mikhailChelbaev Nov 25, 2024
0badac1
reorder layout properties
mikhailChelbaev Nov 25, 2024
616606d
add modal corner radius
mikhailChelbaev Nov 25, 2024
8ab57a4
fix invalid content size calculation inside the modal
mikhailChelbaev Nov 25, 2024
99dd4d7
remove `hasCloseButton` from modalVM
mikhailChelbaev Nov 25, 2024
dbf6042
add custom platform agnostic `Paddings` struct
mikhailChelbaev Nov 25, 2024
c7d6e34
fix: return proper color for `secondaryBackground`
mikhailChelbaev Nov 25, 2024
6d7959b
add preview for `UKCenterModalController`
mikhailChelbaev Nov 25, 2024
557449f
add BottomModal preview
mikhailChelbaev Nov 26, 2024
307abcb
regroup files
mikhailChelbaev Nov 28, 2024
384083f
extract bottom modal animation params into a separate file
mikhailChelbaev Nov 28, 2024
594585d
refactor: modal previews
mikhailChelbaev Nov 29, 2024
296eb83
add `transitionDuration` property to `ModalVM`
mikhailChelbaev Nov 29, 2024
5534029
refactor: add ModalTransition type
mikhailChelbaev Nov 29, 2024
0f446e1
add docs for modal vms
mikhailChelbaev Nov 29, 2024
5cd2fe2
add docs for modal controllers
mikhailChelbaev Nov 29, 2024
c661a60
fix: rename picker param
mikhailChelbaev Nov 29, 2024
95b9f9a
add examples to the docs
mikhailChelbaev Nov 29, 2024
fe30575
merge with refactor/config
mikhailChelbaev Dec 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import ComponentsKit
import SwiftUI
import UIKit

struct ModalPreviewHelpers {
// MARK: - Enums

enum ContentBody {
case shortText
case longText
}
enum ContentFooter {
case button
case buttonAndCheckbox
}

// MARK: - Preview Sections

struct ContentSection<VM: ModalVM>: View {
@Binding var model: VM
@Binding var hasHeader: Bool
@Binding var contentBody: ContentBody
@Binding var contentFooter: ContentFooter?

var body: some View {
Section("Content") {
Picker("Header", selection: self.$hasHeader) {
Text("Text").tag(true)
Text("None").tag(false)
}
Picker("Body", selection: self.$contentBody) {
Text("Short Text").tag(ContentBody.shortText)
Text("Long Text").tag(ContentBody.longText)
}
Picker("Footer", selection: .init(
get: {
return self.contentFooter
},
set: { newValue in
if newValue == nil {
self.model.closesOnOverlayTap = true
}
self.contentFooter = newValue
}
)) {
Text("Button").tag(ContentFooter.button)
Text("Button and Checkbox").tag(ContentFooter.buttonAndCheckbox)
Text("None").tag(Optional<ContentFooter>.none)
}
}
}
}

struct PropertiesSection<VM: ModalVM, Pickers: View>: View {
@Binding var model: VM
@Binding var footer: ContentFooter?
@ViewBuilder var additionalPickers: () -> Pickers

var body: some View {
Section("Properties") {
Picker("Background Color", selection: self.$model.backgroundColor) {
Text("Default").tag(Optional<UniversalColor>.none)
Text("Accent Background").tag(ComponentColor.accent.background)
Text("Success Background").tag(ComponentColor.success.background)
Text("Warning Background").tag(ComponentColor.warning.background)
Text("Danger Background").tag(ComponentColor.danger.background)
}
Toggle("Closes On Overlay Tap", isOn: self.$model.closesOnOverlayTap)
.disabled(self.footer == nil)
Picker("Outer Paddings", selection: self.$model.outerPaddings) {
Text("12px").tag(Paddings(padding: 12))
Text("16px").tag(Paddings(padding: 16))
Text("20px").tag(Paddings(padding: 20))
}
Picker("Content Spacing", selection: self.$model.contentSpacing) {
Text("8px").tag(CGFloat(8))
Text("12px").tag(CGFloat(12))
Text("16px").tag(CGFloat(16))
}
Picker("Content Paddings", selection: self.$model.contentPaddings) {
Text("12px").tag(Paddings(padding: 12))
Text("16px").tag(Paddings(padding: 16))
Text("20px").tag(Paddings(padding: 20))
}
Picker("Corner Radius", selection: self.$model.cornerRadius) {
Text("None").tag(ModalRadius.none)
Text("Small").tag(ModalRadius.small)
Text("Medium").tag(ModalRadius.medium)
Text("Large").tag(ModalRadius.large)
Text("Custom 30px").tag(ModalRadius.custom(30))
}
Picker("Overlay Style", selection: self.$model.overlayStyle) {
Text("Blurred").tag(ModalOverlayStyle.blurred)
Text("Dimmed").tag(ModalOverlayStyle.dimmed)
Text("Transparent").tag(ModalOverlayStyle.transparent)
}
Picker("Size", selection: self.$model.size) {
Text("Small").tag(ModalSize.small)
Text("Medium").tag(ModalSize.medium)
Text("Large").tag(ModalSize.large)
Text("Full").tag(ModalSize.full)
}
Picker("Transition", selection: self.$model.transition) {
Text("None").tag(ModalTransition.none)
Text("Fast").tag(ModalTransition.fast)
Text("Normal").tag(ModalTransition.normal)
Text("Slow").tag(ModalTransition.slow)
}
self.additionalPickers()
}
}
}

// MARK: - Shared UI

private static let headerTitle = "Header"
private static let headerFont: UniversalFont = .system(size: 20, weight: .bold)

private static let bodyShortText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
private static let bodyLongText = """
Lorem ipsum odor amet, consectetuer adipiscing elit. Vitae vehicula pellentesque lectus orci fames. Cras suscipit dui tortor penatibus turpis ultrices. Laoreet montes adipiscing ante dapibus facilisis. Lorem per fames nec duis quis eleifend imperdiet. Tincidunt id interdum adipiscing eros dis quis platea varius. Potenti eleifend eu molestie laoreet varius sapien. Adipiscing nascetur platea penatibus curabitur tempus nibh laoreet porttitor. Augue et curabitur cras sed semper inceptos nunc montes mollis.

Lectus arcu pellentesque inceptos tempor fringilla nascetur. Erat curae convallis integer mi, quis facilisi tortor. Phasellus aliquam molestie vehicula odio in dis maximus diam elit. Rutrum gravida amet euismod feugiat fusce. Est egestas velit vulputate senectus sociosqu fringilla eget nibh. Nam pellentesque aenean mi platea tincidunt quam sem purus. Himenaeos suspendisse nec sapien habitasse ultricies maecenas libero odio. Rutrum senectus maximus ultrices, ad nam ultricies placerat.

Enim habitant laoreet inceptos scelerisque senectus, tellus molestie ut. Eros risus nibh morbi eu aenean. Velit ligula magnis aliquet at luctus. Dapibus vestibulum consectetur euismod vitae per ultrices litora quis. Aptent eleifend dapibus urna lacinia felis nisl. Sit amet fusce nullam feugiat posuere. Urna amet curae velit fermentum interdum vestibulum penatibus. Penatibus vivamus sem ultricies pellentesque congue id mattis diam. Aliquam efficitur mi gravida sollicitudin; amet imperdiet. Rutrum mollis risus justo tortor in duis cursus.
"""
private static let bodyFont: UniversalFont = .system(size: 18, weight: .regular)

private static let footerButtonVM = ButtonVM {
$0.title = "Close"
$0.isFullWidth = true
$0.color = .primary
}
private static let footerCheckboxVM = CheckboxVM {
$0.title = "Agree and continue"
}

// MARK: - UIKit

static func ukHeader(hasHeader: Bool) -> UKModalController.Content? {
guard hasHeader else {
return nil
}

return { _ in
let title = UILabel()
title.text = self.headerTitle
title.font = self.headerFont.uiFont
return title
}
}

static func ukBody(body: ContentBody) -> UKModalController.Content {
return { _ in
let subtitle = UILabel()
switch body {
case .shortText:
subtitle.text = self.bodyShortText
case .longText:
subtitle.text = self.bodyLongText
}
subtitle.numberOfLines = 0
subtitle.font = self.bodyFont.uiFont
return subtitle
}
}

static func ukFooter(footer: ContentFooter?) -> UKModalController.Content? {
return footer.map { footer in
return { dismiss in
let stackView = UIStackView()
stackView.axis = .vertical
stackView.spacing = 16

let button = UKButton(
model: self.footerButtonVM,
action: { dismiss(true) }
)
stackView.addArrangedSubview(button)

switch footer {
case .button:
button.model.isEnabled = true
case .buttonAndCheckbox:
button.model.isEnabled = false
let checkbox = UKCheckbox(
initialValue: false,
model: self.footerCheckboxVM,
onValueChange: { isSelected in
button.model.isEnabled = isSelected
}
)
stackView.insertArrangedSubview(checkbox, at: 0)
}

return stackView
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import UIKit

extension UIApplication {
var topViewController: UIViewController? {
var topViewController: UIViewController?

if #available(iOS 13, *) {
for scene in self.connectedScenes {
if let windowScene = scene as? UIWindowScene {
for window in windowScene.windows {
if window.isKeyWindow {
topViewController = window.rootViewController
}
}
}
}
} else {
topViewController = self.keyWindow?.rootViewController
}

while true {
if let presented = topViewController?.presentedViewController {
topViewController = presented
} else if let navController = topViewController as? UINavigationController {
topViewController = navController.topViewController
} else if let tabBarController = topViewController as? UITabBarController {
topViewController = tabBarController.selectedViewController
} else {
break
}
}
return topViewController
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import ComponentsKit
import SwiftUI
import UIKit

struct BottomModalPreview: View {
@State var model = BottomModalVM()

@State var isModalPresented: Bool = false
@State var isCheckboxSelected: Bool = false

@State var hasHeader = true
@State var contentBody: ModalPreviewHelpers.ContentBody = .shortText
@State var contentFooter: ModalPreviewHelpers.ContentFooter? = .buttonAndCheckbox

var body: some View {
VStack {
PreviewWrapper(title: "UIKit") {
UKButton(model: .init { $0.title = "Show Modal" }) {
UIApplication.shared.topViewController?.present(
UKBottomModalController(
model: self.model,
header: ModalPreviewHelpers.ukHeader(hasHeader: self.hasHeader),
body: ModalPreviewHelpers.ukBody(body: self.contentBody),
footer: ModalPreviewHelpers.ukFooter(footer: self.contentFooter)
),
animated: true
)
}
.preview
}

Form {
ModalPreviewHelpers.ContentSection(
model: self.$model,
hasHeader: self.$hasHeader,
contentBody: self.$contentBody,
contentFooter: self.$contentFooter
)
ModalPreviewHelpers.PropertiesSection(
model: self.$model,
footer: self.$contentFooter,
additionalPickers: {
Toggle("Draggable", isOn: self.$model.isDraggable)
Toggle("Hides On Swap", isOn: self.$model.hidesOnSwap)
}
)
}
}
}
}

#Preview {
BottomModalPreview()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import ComponentsKit
import SwiftUI
import UIKit

struct CenterModalPreview: View {
@State var model = CenterModalVM()

@State var isModalPresented: Bool = false
@State var isCheckboxSelected: Bool = false

@State var hasHeader = true
@State var contentBody: ModalPreviewHelpers.ContentBody = .shortText
@State var contentFooter: ModalPreviewHelpers.ContentFooter? = .buttonAndCheckbox

var body: some View {
VStack {
PreviewWrapper(title: "UIKit") {
UKButton(model: .init { $0.title = "Show Modal" }) {
UIApplication.shared.topViewController?.present(
UKCenterModalController(
model: self.model,
header: ModalPreviewHelpers.ukHeader(hasHeader: self.hasHeader),
body: ModalPreviewHelpers.ukBody(body: self.contentBody),
footer: ModalPreviewHelpers.ukFooter(footer: self.contentFooter)
),
animated: true
)
}
.preview
}
Form {
ModalPreviewHelpers.ContentSection(
model: self.$model,
hasHeader: self.$hasHeader,
contentBody: self.$contentBody,
contentFooter: self.$contentFooter
)
ModalPreviewHelpers.PropertiesSection(
model: self.$model,
footer: self.$contentFooter,
additionalPickers: {
EmptyView()
}
)
}
}
}
}

#Preview {
CenterModalPreview()
}
9 changes: 9 additions & 0 deletions Examples/DemosApp/DemosApp/Core/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ struct App: View {
}
}

Section("Modals") {
NavigationLinkWithTitle("Bottom Modal") {
BottomModalPreview()
}
NavigationLinkWithTitle("Center Modal") {
CenterModalPreview()
}
}

Section("Login Demo") {
NavigationLinkWithTitle("SwiftUI") {
SwiftUILogin()
Expand Down
7 changes: 7 additions & 0 deletions Sources/ComponentsKit/Configuration/Layout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,13 @@ extension ComponentsKitConfig {
large: 16.0
)

/// The radius configuration for content containers such as modals, cards, etc.
public var containerRadius: Radius = .init(
small: 16.0,
medium: 20.0,
large: 26.0
)

/// The border width configuration for components.
public var borderWidth: BorderWidth = .init(
small: 1.0,
Expand Down
7 changes: 7 additions & 0 deletions Sources/ComponentsKit/Helpers/UIView+Helpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,10 @@ extension UIView {
}
}
}

extension UIView {
/// A helper to get bounds of the device's screen.
public var screenBounds: CGRect {
return self.window?.windowScene?.screen.bounds ?? UIScreen.main.bounds
}
}
Loading
Loading