Skip to content

Commit

Permalink
[RadioButton#629] Move single radio button support from polaris to sp…
Browse files Browse the repository at this point in the history
…ark.
  • Loading branch information
michael-zimmermann committed Dec 5, 2023
1 parent b0395c0 commit faaacc8
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 27 deletions.
64 changes: 64 additions & 0 deletions core/Sources/Common/Combine/Publisher/PublishingBinding.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// PublishingBinding.swift
// SparkCore
//
// Created by Michael Zimmermann on 24.11.23.
// Copyright © 2023 Adevinta. All rights reserved.
//

import Combine
import SwiftUI

protocol PublishingBinding {
var publisher: any Publisher<Bool, Never> { get }
}

final class PublisherBinding<ID: Equatable & CustomStringConvertible>: PublishingBinding {
let id: ID
var selectedID: ID?

private lazy var binding = Binding<ID?>(
get: { self.selectedID },
set: { newValue in
self.selectedID = newValue
}
)

private lazy var publishedBinding = PublishedBinding<ID>(binding: self.binding, id: self.id)

var publisher: any Publisher<Bool, Never> {
return publishedBinding.publisher
}

var wrappedBinding: Binding<ID?> {
return self.publishedBinding.wrappedBinding
}

init(id: ID, selectedID: ID?) {
self.id = id
self.selectedID = selectedID
}
}

final class PublishedBinding<ID: Equatable & CustomStringConvertible>: PublishingBinding {
let binding: Binding<ID?>
let id: ID

lazy var wrappedBinding = Binding<ID?>(
get: { self.binding.wrappedValue },
set: { newValue in
self.binding.wrappedValue = newValue
self.subject.send(self.binding.wrappedValue == self.id)
}
)

var publisher: any Publisher<Bool, Never> {
return self.subject
}
private var subject = PassthroughSubject<Bool, Never>()

init(binding: Binding<ID?>, id: ID) {
self.binding = binding
self.id = id
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,15 @@ public final class RadioButtonUIView<ID: Equatable & CustomStringConvertible>: U
set {
self.viewModel.set(selected: newValue)
}
}

public override var isHighlighted: Bool {
get {
return self.toggleView.isPressed
}
set {
self.toggleView.isPressed = newValue
}
}

/// The label of radio button
Expand Down Expand Up @@ -142,13 +150,20 @@ public final class RadioButtonUIView<ID: Equatable & CustomStringConvertible>: U
}
}


/// Changes of the selection state is posted to the publisher.
public var publisher: some Publisher<Bool, Never> {
return self.publisherBinding.publisher.eraseToAnyPublisher()
}

// MARK: - Private Properties
@ScaledUIMetric private var toggleSize = Constants.toggleViewHeight
@ScaledUIMetric private var spacing: CGFloat
@ScaledUIMetric private var textLabelTopSpacing = Constants.textLabelTopSpacing
@ScaledUIMetric private var haloWidth = Constants.haloWidth

private var subscriptions = Set<AnyCancellable>()
private var publisherBinding: PublishingBinding

// MARK: - View properties
private lazy var toggleView: RadioButtonToggleUIView = {
Expand Down Expand Up @@ -198,15 +213,54 @@ public final class RadioButtonUIView<ID: Equatable & CustomStringConvertible>: U
groupState: RadioButtonGroupState = .enabled,
labelPosition: RadioButtonLabelPosition = .right
) {
let publisherBinding = PublishedBinding(binding: selectedID, id: id)

let viewModel = RadioButtonViewModel(
theme: theme,
intent: groupState.intent,
id: id,
label: .left(label),
selectedID: selectedID,
selectedID: publisherBinding.wrappedBinding,
alignment: labelPosition.alignment)

self.init(viewModel: viewModel)
self.init(
viewModel: viewModel,
publisherBinding: publisherBinding
)
}

/// A radio button component which can be used as a standalone component.
/// This convenience init, avoids needing to use a binding. Changes to the selection state are published to the publisher.
///
/// Parameters:
/// - theme: The current theme
/// - intent: The intent defining the color
/// - id: The value of the radio button
/// - label: The text rendered to describe the value
/// - isSelected: Bool, defining whether the radiobutton is selected or not.
/// - labelAlignment: the alignment of the label according to the toggle
public convenience init(
theme: Theme,
intent: RadioButtonIntent,
id: ID,
label: NSAttributedString,
isSelected: Bool,
labelAlignment: RadioButtonLabelAlignment = .trailing
) {
let publisherBinding = PublisherBinding(id: id, selectedID: isSelected ? id : nil)

let viewModel = RadioButtonViewModel(
theme: theme,
intent: intent,
id: id,
label: .left(label),
selectedID: publisherBinding.wrappedBinding,
alignment: labelAlignment)

self.init(
viewModel: viewModel,
publisherBinding: publisherBinding
)
}

/// The radio button component takes a theme, an id, a label and a binding
Expand All @@ -226,19 +280,25 @@ public final class RadioButtonUIView<ID: Equatable & CustomStringConvertible>: U
selectedID: Binding<ID?>,
labelAlignment: RadioButtonLabelAlignment = .trailing
) {
let publisherBinding = PublishedBinding(binding: selectedID, id: id)
let viewModel = RadioButtonViewModel(
theme: theme,
intent: intent,
id: id,
label: .left(label),
selectedID: selectedID,
selectedID: publisherBinding.wrappedBinding,
alignment: labelAlignment)

self.init(viewModel: viewModel)
self.init(
viewModel: viewModel,
publisherBinding: publisherBinding
)
}

init(viewModel: RadioButtonViewModel<ID>) {
init(viewModel: RadioButtonViewModel<ID>,
publisherBinding: PublishingBinding) {
self.viewModel = viewModel
self.publisherBinding = publisherBinding
self._spacing = ScaledUIMetric(wrappedValue: viewModel.spacing)

super.init(frame: CGRect.zero)
Expand Down Expand Up @@ -432,25 +492,6 @@ public final class RadioButtonUIView<ID: Equatable & CustomStringConvertible>: U
}

// MARK: - Control functions
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
guard self.isEnabled else { return }

self.toggleView.isPressed = true
}

public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)

self.toggleView.isPressed = false
}

public override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)

self.toggleView.isPressed = false
}

@IBAction func actionTapped(sender: Any?) {
self.isSelected = true
self.toggleView.isPressed = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,33 @@ import UIKit
final class RadioButtonComponentUIView: ComponentUIView {
// MARK: - Components
private let componentView: RadioButtonUIGroupView<Int>
private let singleComponentView: RadioButtonUIView<Int>
private let stackView: UIStackView

// MARK: - Properties

private let viewModel: RadioButtonComponentUIViewModel
private var cancellables = Set<AnyCancellable>()
private var singleRadioButtonValuePublished = false

// MARK: - Initializer
init(viewModel: RadioButtonComponentUIViewModel) {
self.viewModel = viewModel
let componentView = Self.makeRadioButtonView(viewModel)
self.componentView = componentView
let singleComponentView = Self.makeSingleRadioButtonView(viewModel)
self.singleComponentView = singleComponentView

let stackView = UIStackView(arrangedSubviews: [componentView, singleComponentView])
stackView.axis = NSLayoutConstraint.Axis.vertical
stackView.spacing = 20

self.stackView = stackView


super.init(
viewModel: viewModel,
componentView: componentView
componentView: stackView
)

self.setupSubscriptions()
Expand All @@ -50,18 +62,21 @@ final class RadioButtonComponentUIView: ComponentUIView {
self.viewModel.configurationViewModel.update(theme: theme)

self.componentView.theme = theme
self.singleComponentView.theme = theme
}

self.viewModel.$intent.subscribe(in: &self.cancellables) { [weak self] intent in
guard let self = self else { return }
self.viewModel.intentConfigurationItemViewModel.buttonTitle = intent.name
self.componentView.intent = intent
self.singleComponentView.intent = intent
}

self.viewModel.$labelAlignment.subscribe(in: &self.cancellables) { [weak self] alignment in
guard let self = self else { return }
self.viewModel.alignmentConfigurationItemViewModel.buttonTitle = alignment.name
self.componentView.labelAlignment = alignment
self.singleComponentView.labelAlignment = alignment
}

self.viewModel.$axis.subscribe(in: &self.cancellables) { [weak self] axis in
Expand Down Expand Up @@ -105,6 +120,12 @@ final class RadioButtonComponentUIView: ComponentUIView {

self.viewModel.$isDisabled.subscribe(in: &self.cancellables) { [weak self] disabled in
self?.componentView.isEnabled = !disabled

self?.singleComponentView.isEnabled = !disabled
}

self.viewModel.$isSelected.subscribe(in: &self.cancellables) { [weak self] selected in
self?.singleComponentView.isSelected = selected
}

self.viewModel.$numberOfRadioButtons.subscribe(in: &self.cancellables) { [weak self] numberOfRadioButtons in
Expand All @@ -120,10 +141,32 @@ final class RadioButtonComponentUIView: ComponentUIView {
self.componentView.addRadioButton(content)
}
}

self.componentView.radioButtonViews[0].publisher.subscribe(in: &self.cancellables) { selected in
print("GROUP VALUE PUBLISHED \(selected)")
}
self.singleComponentView.publisher
.subscribe(in: &self.cancellables) {
[weak self] selected in
guard let self = self else { return }
print("VALUE PUBLISHED \(selected)")
self.singleRadioButtonValuePublished = selected
}

let action = UIAction { [weak self] action in
guard let self = self else { return }
print("ACTION HANDLE")
if !self.singleRadioButtonValuePublished {
self.singleComponentView.isSelected = false
}
self.singleRadioButtonValuePublished = false
}
self.singleComponentView.addAction(action, for: .touchUpInside)
}

// MARK: - Private construction helper
static private func makeRadioButtonView(_ viewModel: RadioButtonComponentUIViewModel) -> RadioButtonUIGroupView<Int> {

let component = RadioButtonUIGroupView(
theme: viewModel.theme,
intent: viewModel.intent,
Expand All @@ -135,7 +178,15 @@ final class RadioButtonComponentUIView: ComponentUIView {

component.title = "Radio Button Group (UIKit)"
component.supplementaryText = "Radio Button Group Supplementary Text"

return component
}

static private func makeSingleRadioButtonView(_ viewModel: RadioButtonComponentUIViewModel) -> RadioButtonUIView<Int> {
return RadioButtonUIView(
theme: viewModel.theme,
intent: viewModel.intent,
id: 99,
label: NSAttributedString(string: "Sample of toggle on radio button"),
isSelected: false)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ final class RadioButtonComponentUIViewModel: ComponentUIViewModel {
target: (source: self, action: #selector(self.disableChanged(_:))))
}()

lazy var selectedConfigurationItemViewModel: ComponentsConfigurationItemUIViewModel = {
return .init(
name: "Selected (for single radio button)",
type: .checkbox(title: "", isOn: self.isSelected),
target: (source: self, action: #selector(self.selectedChanged(_:))))
}()

lazy var numberOfRadioButtonsConfigurationItemViewModel: ComponentsConfigurationItemUIViewModel = {
return .init(
name: "Number of Items",
Expand Down Expand Up @@ -110,7 +117,8 @@ final class RadioButtonComponentUIViewModel: ComponentUIViewModel {
self.longLabelConfigurationItemViewModel,
self.attributedLabelConfigurationItemViewModel,
self.disableConfigurationItemViewModel,
self.numberOfRadioButtonsConfigurationItemViewModel
self.numberOfRadioButtonsConfigurationItemViewModel,
self.selectedConfigurationItemViewModel
]
}

Expand All @@ -127,6 +135,7 @@ final class RadioButtonComponentUIViewModel: ComponentUIViewModel {
@Published var showIcon = true
@Published var showBadge = false
@Published var isDisabled = false
@Published var isSelected = false // only for single radio button
@Published var numberOfRadioButtons = 3
@Published var selectedRadioButton = 0
@Published var axis: RadioButtonGroupLayout = .vertical
Expand Down Expand Up @@ -210,6 +219,10 @@ extension RadioButtonComponentUIViewModel {
self.isDisabled = isTrue(selected)
}

@objc func selectedChanged(_ selected: Any?) {
self.isSelected = isTrue(selected)
}

@objc func axisChanged(_ selected: Any?) {
self.axis = isTrue(selected) ? .vertical : .horizontal
}
Expand Down

0 comments on commit faaacc8

Please sign in to comment.