Skip to content

Commit

Permalink
[KOA-5077] BPKDialog SwiftUI (#1631)
Browse files Browse the repository at this point in the history
* dialog basis

* flare view and path

* dialog

* readme

* code docs

* different dialog approach

* unsanted change

* aligning examples and screenshots
  • Loading branch information
frugoman committed Apr 14, 2023
1 parent 1944985 commit e7d3618
Show file tree
Hide file tree
Showing 51 changed files with 1,341 additions and 198 deletions.
3 changes: 3 additions & 0 deletions Backpack-SwiftUI.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,8 @@ Pod::Spec.new do |s|
s.test_spec 'Tests' do |test_spec|
test_spec.dependency 'SnapshotTesting', '~> 1.9.0'
test_spec.source_files = 'Backpack-SwiftUI/Tests/**/*.{swift,png}'
test_spec.ios.resource_bundle = {
'UnitTestsImages' => 'Backpack-SwiftUI/Tests/Images*'
}
end
end
8 changes: 8 additions & 0 deletions Backpack-SwiftUI/Button/Classes/BPKButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public struct BPKButton: View {
private let icon: Icon?
private let size: BPKButton.Size
private var style: BPKButton.Style = .primary
private var matchesParentWidth = false
private var accessibilityLabel: String
private let action: () -> Void

Expand Down Expand Up @@ -102,13 +103,20 @@ public struct BPKButton: View {
style: style,
size: size,
iconOnly: isIconOnly,
matchesParentWidth: matchesParentWidth,
getCurrentState: currentState(isPressed:),
colorProvider: ButtonColorProvider(
colorSetFactory: DefaultButtonColorSetFactory()
)
)
}

public func stretchable() -> BPKButton {
var result = self
result.matchesParentWidth = true
return result
}

private func currentState(isPressed: Bool) -> BPKButton.CurrentState {
if !enabled { return .disabled }
if loading { return .loading }
Expand Down
2 changes: 2 additions & 0 deletions Backpack-SwiftUI/Button/Classes/CurrentStateButtonStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ struct CurrentStateButtonStyle: ButtonStyle {
let style: BPKButton.Style
let size: BPKButton.Size
let iconOnly: Bool
let matchesParentWidth: Bool
let getCurrentState: (_ isPressed: Bool) -> BPKButton.CurrentState
let colorProvider: ButtonColorProvider

Expand Down Expand Up @@ -49,6 +50,7 @@ struct CurrentStateButtonStyle: ButtonStyle {

configuration.label
.frame(width: width, height: height)
.frame(maxWidth: matchesParentWidth ? .infinity : nil)
.padding([.leading, .trailing], sidesPadding)
.background(background)
.foregroundColor(foreground)
Expand Down
167 changes: 167 additions & 0 deletions Backpack-SwiftUI/Dialog/Classes/BPKDialog.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*
* Backpack - Skyscanner's Design System
*
* Copyright 2018 Skyscanner Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import SwiftUI

public extension View {
/// Displays a success dialog with a title, text and a list of buttons.
func bpkSuccessDialog(
presented: Binding<Bool>,
icon: BPKIcon,
title: String,
text: String,
confirmButton: BPKDialogButton,
secondaryActions: BPKDialogSecondaryActions? = nil,
onTouchOutside: (() -> Void)? = nil
) -> some View {
var buttons = [BPKDialogButton(button: confirmButton, style: .featured)]
if let secondaryActions {
buttons.append(BPKDialogButton(button: secondaryActions.secondaryButton, style: .secondary))
if let linkButton = secondaryActions.linkButton {
buttons.append(BPKDialogButton(button: linkButton, style: .link))
}
}
return self.modifier(DialogContainerViewModifier(isPresented: presented, dialogContent: {
DialogWithIconContent(
icon: icon,
iconColor: .coreAccentColor,
textContent: DialogTextContent(title: title, text: text, contentAlignment: .center),
actions: DialogActionsView(buttons: buttons)
)
}, onTouchOutside: onTouchOutside))
}

/// Displays a warning dialog with a title, text and a list of buttons.
func bpkWarningDialog(
presented: Binding<Bool>,
icon: BPKIcon,
title: String,
text: String,
confirmButton: BPKDialogButton,
secondaryActions: BPKDialogSecondaryActions? = nil,
onTouchOutside: (() -> Void)? = nil
) -> some View {
var buttons = [BPKDialogButton(button: confirmButton, style: .featured)]
if let secondaryActions {
buttons.append(BPKDialogButton(button: secondaryActions.secondaryButton, style: .secondary))
if let linkButton = secondaryActions.linkButton {
buttons.append(BPKDialogButton(button: linkButton, style: .link))
}
}
return self.modifier(DialogContainerViewModifier(isPresented: presented, dialogContent: {
DialogWithIconContent(
icon: icon,
iconColor: .statusWarningSpotColor,
textContent: DialogTextContent(title: title, text: text, contentAlignment: .center),
actions: DialogActionsView(buttons: buttons)
)
}, onTouchOutside: onTouchOutside))
}

/// Displays a destructive dialog with a title, text and a list of buttons.
func bpkDestructiveDialog(
presented: Binding<Bool>,
icon: BPKIcon,
title: String,
text: String,
confirmButton: BPKDialogButton,
linkButton: BPKDialogButton? = nil,
onTouchOutside: (() -> Void)? = nil
) -> some View {
var buttons = [BPKDialogButton(button: confirmButton, style: .destructive)]
if let linkButton {
buttons.append(BPKDialogButton(button: linkButton, style: .link))
}
return self.modifier(DialogContainerViewModifier(isPresented: presented, dialogContent: {
DialogWithIconContent(
icon: icon,
iconColor: .statusDangerSpotColor,
textContent: DialogTextContent(title: title, text: text, contentAlignment: .center),
actions: DialogActionsView(buttons: buttons)
)
}, onTouchOutside: onTouchOutside))
}

/// Displays a dialog with an image, title, text and a list of buttons.
func bpkImageDialog(
presented: Binding<Bool>,
image: Image,
title: String,
text: String,
confirmButton: BPKDialogButton,
secondaryActions: BPKDialogSecondaryActions? = nil,
onTouchOutside: (() -> Void)? = nil
) -> some View {
var buttons = [BPKDialogButton(button: confirmButton, style: .featured)]
if let secondaryActions {
buttons.append(BPKDialogButton(button: secondaryActions.secondaryButton, style: .secondary))
if let linkButton = secondaryActions.linkButton {
buttons.append(BPKDialogButton(button: linkButton, style: .link))
}
}
return self.modifier(DialogContainerViewModifier(isPresented: presented, dialogContent: {
DialogWithHeaderContent(
textContent: DialogTextContent(title: title, text: text, contentAlignment: .leading)
.spacing(.md),
actions: DialogActionsView(buttons: buttons)
) { DialogImageView(image: image) }
}, onTouchOutside: onTouchOutside))
}

/// Displays a dialog with an image with a flare, title, text and a list of buttons.
func bpkFlareDialog(
presented: Binding<Bool>,
image: Image,
title: String,
text: String,
confirmButton: BPKDialogButton,
secondaryActions: BPKDialogSecondaryActions? = nil,
onTouchOutside: (() -> Void)? = nil
) -> some View {
var buttons = [BPKDialogButton(button: confirmButton, style: .featured)]
if let secondaryActions {
buttons.append(BPKDialogButton(button: secondaryActions.secondaryButton, style: .secondary))
if let linkButton = secondaryActions.linkButton {
buttons.append(BPKDialogButton(button: linkButton, style: .link))
}
}
return self.modifier(DialogContainerViewModifier(isPresented: presented, dialogContent: {
DialogWithHeaderContent(
textContent: DialogTextContent(title: title, text: text, contentAlignment: .center),
actions: DialogActionsView(buttons: buttons)
) {
BPKFlareView(roundedCorners: false) {
DialogImageView(image: image)
}
}
}, onTouchOutside: onTouchOutside))
}
}

struct BPKDialog_Previews: PreviewProvider {
static var previews: some View {
Color(.canvasColor)
.bpkSuccessDialog(
presented: .constant(true),
icon: .tick,
title: "Title in here",
text: "Description that goes two lines ideally, but sometimes it can go longer",
confirmButton: .init("Delete", action: {})
)
}
}
63 changes: 63 additions & 0 deletions Backpack-SwiftUI/Dialog/Classes/BPKDialogButton.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Backpack - Skyscanner's Design System
*
* Copyright 2018 Skyscanner Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/// A button to be used in a dialog.
public struct BPKDialogButton {
let title: String
let action: () -> Void
let style: BPKButton.Style

/// Creates a new instance of `BPKDialogButton`. The button styling will be
/// managed by the dialog.
///
/// - Parameters:
/// - title: The title of the button.
/// - action: The action to be performed when the button is tapped.
public init(_ title: String, action: @escaping () -> Void) {
self.title = title
self.action = action
style = .secondary
}

init(_ title: String, _ style: BPKButton.Style, action: @escaping () -> Void) {
self.title = title
self.action = action
self.style = style
}

init(button: BPKDialogButton, style: BPKButton.Style) {
title = button.title
action = button.action
self.style = style
}
}

public struct BPKDialogSecondaryActions {
let secondaryButton: BPKDialogButton
let linkButton: BPKDialogButton?

public init(secondaryButton: BPKDialogButton, linkButton: BPKDialogButton) {
self.secondaryButton = secondaryButton
self.linkButton = linkButton
}

public init(secondaryButton: BPKDialogButton) {
self.secondaryButton = secondaryButton
linkButton = nil
}
}
46 changes: 46 additions & 0 deletions Backpack-SwiftUI/Dialog/Classes/DialogActionsView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Backpack - Skyscanner's Design System
*
* Copyright 2018 Skyscanner Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import SwiftUI

/// View that displays a list of buttons in a vertical stack for use in a dialog.
struct DialogActionsView: View {
let buttons: [BPKDialogButton]

var body: some View {
VStack(spacing: BPKSpacing.md.value) {
ForEach(buttons, id: \.title) { button in
BPKButton(button.title, size: .large, action: button.action)
.buttonStyle(button.style)
.stretchable()
}
}
}
}

struct DialogActionsView_Previews: PreviewProvider {
static var previews: some View {
DialogActionsView(
buttons: [
BPKDialogButton("Confirm", .featured, action: {}),
BPKDialogButton("Skip", .secondary, action: {}),
BPKDialogButton("More info", .link, action: {})
]
)
}
}

0 comments on commit e7d3618

Please sign in to comment.