I am having a strange problem with UIButton. I have the following custom class:
import UIKit
#IBDesignable class ToggleButton: UIButton {
#IBInspectable var state1Image: UIImage = UIImage()
#IBInspectable var state2Image: UIImage = UIImage()
#IBInspectable var someString: String = ""
private var toogleOn: Bool = false {
didSet {
if toogleOn {
isSelected = true
} else {
isSelected = false
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup() {
print("test")
print("some string is \(someString)")
}
#objc func didToggleButton() {
toogleOn = !toogleOn
}
}
In the interface builder I set the inspectable vars, let's say I set someString to hello. Now when I run the app and view the log the print for the var is "". Also I am unable to set the images. It only uses the default values and will not use the new value that I set. What am I doing wrong here?
Try this:
#IBDesignable class ToggleButton: UIButton {
#IBInspectable var state1Image: UIImage = UIImage() {
didSet {
setup()
}
}
#IBInspectable var state2Image: UIImage = UIImage() {
didSet {
setup()
}
}
#IBInspectable var someString: String = "" {
didSet {
setup()
}
}
override func prepareForInterfaceBuilder() {
setup()
}
private func setup() {
print("test")
// Updating title label as someString to see the update
self.titleLabel?.text = someString
}
private var toogleOn: Bool = false {
didSet {
if toogleOn {
isSelected = true
} else {
isSelected = false
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
#objc func didToggleButton() {
toogleOn = !toogleOn
}
}
Related
How to implement callback for example of button tap inside UICollectionViewCell's UIContentView or better how to do it in Combine way?
Cell registration inside UICollectionView:
let cellRegistration = UICollectionView.CellRegistration<MyCell, Item> { (cell, indexPath, item) in
cell.item = item
}
Cell:
class MyCell: UICollectionViewCell {
var item: Item?
override func updateConfiguration(using state: UICellConfigurationState) {
var newConfiguration = MyContentConfiguration().updated(for: state)
newConfiguration.name = item?.title
contentConfiguration = newConfiguration
}
}
Content configuration:
struct MyContentConfiguration: UIContentConfiguration, Hashable {
var name: String?
func makeContentView() -> UIView & UIContentView {
return MyContentView(configuration: self)
}
}
Content view:
class MyContentView: UIView, UIContentView {
let title = UILabel()
private var currentConfiguration: MyContentConfiguration!
var configuration: UIContentConfiguration {
get {
currentConfiguration
}
set {
guard let newConfiguration = newValue as? MyContentConfiguration else {
return
}
apply(configuration: newConfiguration)
}
}
init(configuration: MyContentConfiguration) {
super.init(frame: .zero)
// Create the content view UI
setupUI()
apply(configuration: configuration)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private extension MyContentView {
private func setupUI() {
// UI stuff
}
private func apply(configuration: MyContentConfiguration) {
guard currentConfiguration != configuration else {
return
}
currentConfiguration = configuration
// Set data to UI elements
title.text = title
//etc..
}
}
Inside MyContentView there'll be a button for example. This button should be bound with item, so when it's tapped - some callback or Combine publisher should emit value, which I've got to catch inside my collection view.
How I've implemented this previously:
let cellRegistration = UICollectionView.CellRegistration<MyCell, Item> { (cell, indexPath, item) in
cell.item = item
//Catch value which is bound with associated item
cell.somePublisher
.sink { [weak self] in
guard let self = self else { return }
self.subscribePublisher.send($0)
}
.store(in: &self.subscriptions)
}
Cell:
class MyCell: UICollectionViewCell {
var item: Item?
public private(set) var somePublisher = CurrentValueSubject<Bool?, Never>(nil)
//UI setup, etc..
//Emit value for a button tap gesture
#objc func handleTap() {
somePublisher.send(true)
}
}
Is it possible to implement this behavior using UIContentView & UIContentConfiguration?
I'm trying to capture ProximitySensor activity on SwiftUI.
So I've created a class ProximityOberver and trying to update the attribute 'state' in the notification:
import SwiftUI
import UIKit
class ProximityObserver {
#State var state = false;
#objc func didChange(notification: NSNotification) {
print("MyView::ProximityObserver.didChange")
if let device = notification.object as? UIDevice {
print(device.proximityState)
state = device.proximityState
print(state)
}
}
}
struct ContentView: View {
#State var proximityObserver = ProximityObserver()
func activateProximitySensor() {
print("MyView::activateProximitySensor")
if !UIDevice.current.isProximityMonitoringEnabled {
UIDevice.current.isProximityMonitoringEnabled = true
if UIDevice.current.isProximityMonitoringEnabled {
NotificationCenter.default.addObserver(proximityObserver, selector: #selector(proximityObserver.didChange), name: UIDevice.proximityStateDidChangeNotification, object: UIDevice.current)
}
}
}
func deactivateProximitySensor() {
print("MyView::deactivateProximitySensor")
UIDevice.current.isProximityMonitoringEnabled = false
NotificationCenter.default.removeObserver(proximityObserver, name: UIDevice.proximityStateDidChangeNotification, object: UIDevice.current)
}
var body: some View {
Text(proximityObserver.state ? "true" : "false" )
.animation(.linear(duration: 20).delay(20), value: proximityObserver.state)
.onAppear() {
self.activateProximitySensor()
}
.onDisappear() {
self.deactivateProximitySensor()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
But even 'state = device.proximityState' code executed, the following print(state) shows the attribute never changed.
MyView::ProximityObserver.didChange
true
false
Can someone explain why this happens, and how to fix this?
Thank you for the comment.
I could fix this as suggested.
class ProximityObserver: ObservableObject {
#Published var state = false;
#objc func didChange(notification: NSNotification) {
print("MyView::ProximityObserver.didChange")
if let device = notification.object as? UIDevice {
print(device.proximityState)
self.state = device.proximityState
print(state, device.proximityState)
}
}
}
struct ContentView: View {
#ObservedObject var proximityObserver = ProximityObserver()
...
I have a view controller in my app that plays user videos and you can scroll to the right to go to the next video. I have a cell set up that loads the video from the URL in firebase storage and loads the other data. Here is the cell code:
import UIKit
import AVFoundation
protocol ClipsCollectionViewCellDelegate: AnyObject {
func didTapProfile(with model: VideoModel)
func didTapShare(with model: VideoModel)
func didTapNewClip(with model: VideoModel)
}
class ClipsCollectionViewCell: UICollectionViewCell {
static let identifier = "ClipsCollectionViewCell"
// Labels
private let usernameLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.textColor = UIColor.systemPink.withAlphaComponent(0.5)
label.backgroundColor = UIColor.systemPink.withAlphaComponent(0.1)
label.clipsToBounds = true
label.layer.cornerRadius = 8
return label
}()
// Buttons
private let profileButton: UIButton = {
let button = UIButton()
button.setBackgroundImage(UIImage(systemName: "person.circle"), for: .normal)
button.tintColor = .white
button.backgroundColor = UIColor.systemBlue.withAlphaComponent(0.1)
button.clipsToBounds = true
button.layer.cornerRadius = 32
button.isUserInteractionEnabled = true
return button
}()
private let shareButton: UIButton = {
let button = UIButton()
button.setBackgroundImage(UIImage(systemName: "square.and.arrow.down"), for: .normal)
button.tintColor = .white
button.backgroundColor = UIColor.systemBlue.withAlphaComponent(0.1)
button.clipsToBounds = true
button.layer.cornerRadius = 4
button.isUserInteractionEnabled = true
return button
}()
private let newClipButton: UIButton = {
let button = UIButton()
button.setBackgroundImage(UIImage(systemName: "plus"), for: .normal)
button.tintColor = .systemOrange
button.backgroundColor = UIColor.systemOrange.withAlphaComponent(0.1)
button.clipsToBounds = true
button.layer.cornerRadius = 25
button.isUserInteractionEnabled = true
return button
}()
private let videoContainer = UIView()
// Delegate
weak var delegate: ClipsCollectionViewCellDelegate?
// Subviews
var player: AVPlayer?
private var model: VideoModel?
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = .black
contentView.clipsToBounds = true
addSubviews()
}
private func addSubviews() {
contentView.addSubview(videoContainer)
contentView.addSubview(usernameLabel)
contentView.addSubview(profileButton)
contentView.addSubview(shareButton)
contentView.addSubview(newClipButton)
// Add actions
profileButton.addTarget(self, action: #selector(didTapProfileButton), for: .touchUpInside)
shareButton.addTarget(self, action: #selector(didTapShareButton), for: .touchUpInside)
newClipButton.addTarget(self, action: #selector(didTapNewClipButton), for: .touchUpInside)
videoContainer.clipsToBounds = true
contentView.sendSubviewToBack(videoContainer)
}
#objc private func didTapProfileButton() {
guard let model = model else {
return
}
delegate?.didTapProfile(with: model)
}
#objc private func didTapShareButton() {
guard let model = model else {
return
}
delegate?.didTapShare(with: model)
}
#objc private func didTapNewClipButton() {
guard let model = model else {
return
}
delegate?.didTapNewClip(with: model)
}
override func layoutSubviews() {
super.layoutSubviews()
videoContainer.frame = contentView.bounds
let size = contentView.frame.size.width/7
let width = contentView.frame.size.width
let height = contentView.frame.size.height
// Labels
usernameLabel.frame = CGRect(x: (width-(size*3))/2, y: height-880-(size/2), width: size*3, height: size)
// Buttons
profileButton.frame = CGRect(x: width-(size*7), y: height-850-size, width: size, height: size)
shareButton.frame = CGRect(x: width-size, y: height-850-size, width: size, height: size)
newClipButton.frame = CGRect(x: width-size-10, y: height-175-size, width: size/1.25, height: size/1.25)
}
override func prepareForReuse() {
super.prepareForReuse()
usernameLabel.text = nil
player?.pause()
player?.seek(to: CMTime.zero)
}
public func configure(with model: VideoModel) {
self.model = model
configureVideo()
// Labels
usernameLabel.text = model.username
}
private func configureVideo() {
guard let model = model else {
return
}
guard let url = URL(string: model.videoFileURL) else { return }
player = AVPlayer(url: url)
let playerView = AVPlayerLayer()
playerView.player = player
playerView.frame = contentView.bounds
playerView.videoGravity = .resizeAspect
videoContainer.layer.addSublayer(playerView)
player?.volume = 0
player?.play()
player?.actionAtItemEnd = .none
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Here is the view controller code:
import UIKit
struct VideoModel {
let username: String
let videoFileURL: String
}
class BetaClipsViewController: UIViewController, UICollectionViewDelegate {
private var collectionView: UICollectionView?
private var data = [VideoModel]()
/// Notification observer
private var observer: NSObjectProtocol?
/// All post models
private var allClips: [(clip: Clip, owner: String)] = []
private var viewModels = [[ClipFeedCellType]]()
override func viewDidLoad() {
super.viewDidLoad()
title = ""
// for _ in 0..<10 {
// let model = VideoModel(username: "#CJMJM",
// videoFileURL: "https://firebasestorage.googleapis.com:443/v0/b/globe-e8b7f.appspot.com/o/clipvideos%2F1637024382.mp4?alt=media&token=c12d0481-f834-4a17-8eee-30595bdf0e8b")
// data.append(model)
// }
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.itemSize = CGSize(width: view.frame.size.width,
height: view.frame.size.height)
layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView?.register(ClipsCollectionViewCell.self,
forCellWithReuseIdentifier: ClipsCollectionViewCell.identifier)
collectionView?.isPagingEnabled = true
collectionView?.delegate = self
collectionView?.dataSource = self
view.addSubview(collectionView!)
fetchClips()
observer = NotificationCenter.default.addObserver(
forName: .didPostNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.viewModels.removeAll()
self?.fetchClips()
}
self.collectionView?.reloadData()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
collectionView?.frame = view.bounds
}
private func fetchClips() {
guard let username = UserDefaults.standard.string(forKey: "username") else {
return
}
let userGroup = DispatchGroup()
userGroup.enter()
var allClips: [(clip: Clip, owner: String)] = []
DatabaseManager.shared.users(for: username) { usernames in
defer {
userGroup.leave()
}
let users = usernames + [username]
for current in users {
userGroup.enter()
DatabaseManager.shared.clips(for: current) { result in
DispatchQueue.main.async {
defer {
userGroup.leave()
}
switch result {
case .success(let clips):
allClips.append(contentsOf: clips.compactMap({
(clip: $0, owner: current)
}))
case .failure:
break
}
}
}
}
}
userGroup.notify(queue: .main) {
let group = DispatchGroup()
self.allClips = allClips
allClips.forEach { model in
group.enter()
self.createViewModel(
model: model.clip,
username: model.owner,
completion: { success in
defer {
group.leave()
}
if !success {
print("failed to create VM")
}
}
)
}
group.notify(queue: .main) {
self.collectionView?.reloadData()
}
}
}
}
extension BetaClipsViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return viewModels.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return viewModels[section].count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cellType = viewModels[indexPath.section][indexPath.row]
switch cellType {
case .clip(let viewModel):
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ClipsCollectionViewCell.identifier,
for: indexPath)
as? ClipsCollectionViewCell else {
fatalError()
}
cell.delegate = self
cell.configure(with: viewModel)
return cell
}
}
}
extension BetaClipsViewController: ClipsCollectionViewCellDelegate {
func didTapProfile(with model: VideoModel) {
print("profile tapped")
let owner = model.username
DatabaseManager.shared.findUser(username: owner) { [weak self] user in
DispatchQueue.main.async {
guard let user = user else {
return
}
let vc = ProfileViewController(user: user)
self?.navigationController?.pushViewController(vc, animated: true)
}
}
}
func didTapShare(with model: VideoModel) {
print("profile share")
}
func didTapNewClip(with model: VideoModel) {
let vc = RecordViewController()
navigationController?.pushViewController(vc, animated: true)
}
}
extension BetaClipsViewController {
func createViewModel(
model: Clip,
username: String,
completion: #escaping (Bool) -> Void
) {
StorageManager.shared.profilePictureURL(for: username) { [weak self] profilePictureURL in
guard let clipURL = URL(string: model.clipUrlString),
let profilePhotoUrl = profilePictureURL else {
return
}
let clipData: [ClipFeedCellType] = [
.clip(viewModel: VideoModel(username: username,
videoFileURL: model.clipUrlString))
]
self?.viewModels.append(clipData)
completion(true)
}
}
}
I think everything is set up properly in the code so why do no cells show in the collectionView? The view controller shows up as completely blank besides the background color.
I have a custom search bar class, and I used interface builder to insert a search bar as an instance of this custom class. How do I use this search bar is search bar for the UISearchController? Its searchBar property is read-only so I can't assign my search bar to it.
You have to set your already initialized custom search bar to be returned in the overriden search bar getter for you custom UISearchController class and setup its properties after the searchcontroller init, this way all the UISearchController functionality are retained:
public class DSearchController: UISearchController {
private var customSearchBar = DSearchBar()
override public var searchBar: UISearchBar {
get {
return customSearchBar
}
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
public init(searchResultsController: UIViewController?,
searchResultsUpdater: UISearchResultsUpdating?,
delegate: UISearchControllerDelegate?,
dimsBackgroundDuringPresentation: Bool,
hidesNavigationBarDuringPresentation: Bool,
searchBarDelegate: UISearchBarDelegate?,
searchBarFrame: CGRect?,
searchBarStyle: UISearchBarStyle,
searchBarPlaceHolder: String,
searchBarFont: UIFont?,
searchBarTextColor: UIColor?,
searchBarBarTintColor: UIColor?, // Bar background
searchBarTintColor: UIColor) { // Cursor and bottom line
super.init(searchResultsController: searchResultsController)
self.searchResultsUpdater = searchResultsUpdater
self.delegate = delegate
self.dimsBackgroundDuringPresentation = dimsBackgroundDuringPresentation
self.hidesNavigationBarDuringPresentation = hidesNavigationBarDuringPresentation
customSearchBar.setUp(searchBarDelegate,
frame: searchBarFrame,
barStyle: searchBarStyle,
placeholder: searchBarPlaceHolder,
font: searchBarFont,
textColor: searchBarTextColor,
barTintColor: searchBarBarTintColor,
tintColor: searchBarTintColor)
}
}
And this is my custom searchBar:
public class DSearchBar: UISearchBar {
var preferredFont: UIFont?
var preferredTextColor: UIColor?
init(){
super.init(frame: CGRect.zero)
}
func setUp(delegate: UISearchBarDelegate?,
frame: CGRect?,
barStyle: UISearchBarStyle,
placeholder: String,
font: UIFont?,
textColor: UIColor?,
barTintColor: UIColor?,
tintColor: UIColor?) {
self.delegate = delegate
self.frame = frame ?? self.frame
self.searchBarStyle = searchBarStyle
self.placeholder = placeholder
self.preferredFont = font
self.preferredTextColor = textColor
self.barTintColor = barTintColor ?? self.barTintColor
self.tintColor = tintColor ?? self.tintColor
self.bottomLineColor = tintColor ?? UIColor.clearColor()
sizeToFit()
// translucent = false
// showsBookmarkButton = false
// showsCancelButton = true
// setShowsCancelButton(false, animated: false)
// customSearchBar.backgroundImage = UIImage()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
let bottomLine = CAShapeLayer()
var bottomLineColor = UIColor.clearColor()
override public func layoutSubviews() {
super.layoutSubviews()
for view in subviews {
if let searchField = view as? UITextField { setSearchFieldAppearance(searchField); break }
else {
for sView in view.subviews {
if let searchField = sView as? UITextField { setSearchFieldAppearance(searchField); break }
}
}
}
bottomLine.path = UIBezierPath(rect: CGRectMake(0.0, frame.size.height - 1, frame.size.width, 1.0)).CGPath
bottomLine.fillColor = bottomLineColor.CGColor
layer.addSublayer(bottomLine)
}
func setSearchFieldAppearance(searchField: UITextField) {
searchField.frame = CGRectMake(5.0, 5.0, frame.size.width - 10.0, frame.size.height - 10.0)
searchField.font = preferredFont ?? searchField.font
searchField.textColor = preferredTextColor ?? searchField.textColor
//searchField.backgroundColor = UIColor.clearColor()
//backgroundImage = UIImage()
}
}
Init example:
searchController = DSearchController(searchResultsController: ls,
searchResultsUpdater: self,
delegate: self,
dimsBackgroundDuringPresentation: true,
hidesNavigationBarDuringPresentation: true,
searchBarDelegate: ls,
searchBarFrame: CGRectMake(0.0, 0.0, SCREEN_WIDTH, 44.0),
searchBarStyle: .Minimal,
searchBarPlaceHolder: NSLocalizedString("Search a location...", comment: ""),
searchBarFont: nil,
searchBarTextColor: nil,
searchBarBarTintColor: UIColor.whiteColor(),
searchBarTintColor: iconsColor)
searchController.searchBar.keyboardAppearance = .Dark
definesPresentationContext = true
tableView.tableHeaderView = searchController.searchBar
I cannot get my custom button to live preview basic examples and this is putting me off using what would otherwise be a great help to my development (IBDesignable).
My custom button code is as follows:
import Cocoa
#IBDesignable class MyButton: NSButton {
#IBInspectable var name:String = "Bob"{
didSet{
setup()
}
}
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
override func prepareForInterfaceBuilder() {
setup()
}
func setup(){
self.title = name
self.setNeedsDisplay()
}
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
// Drawing code here.
}
}
I then drag either a Custom view OR a NSButton onto my canvas (mainmenu.xib) and adjust its class type in the inspector window to MyButton. The inspectable field pops up and there are no errors BUT my custom button does NOT change its name when I change its value in the property panel!
Further, when I drag a custom view onto the canvas all I get is a blank/transparent rectangle in place of a button (after changing the class to MyButton).
Any assistance would be greatly appreciated. This has been driving me nuts!
I had to put the button inside a NSView:
#IBDesignable
public class MyButton: NSView {
#IBInspectable var name: String = "Bob" {
didSet{
button?.title = name
}
}
public var touchUpHandler: (() -> Void)?
private weak var button: NSButton!
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
configureView()
}
required public init?(coder: NSCoder) {
super.init(coder: coder)
configureView()
}
private func configureView() {
let button = NSButton(title: name, target: self, action: #selector(didTapButton(_:)))
button.bezelStyle = .regularSquare
button.translatesAutoresizingMaskIntoConstraints = false
addSubview(button)
NSLayoutConstraint.activate([
button.topAnchor.constraint(equalTo: topAnchor),
button.leftAnchor.constraint(equalTo: leftAnchor),
button.rightAnchor.constraint(equalTo: rightAnchor),
button.bottomAnchor.constraint(equalTo: bottomAnchor)
])
self.button = button
}
func didTapButton(_ sender: NSButton) {
touchUpHandler?()
}
}
And then I used it like so:
#IBOutlet weak var myButton: MyButton!
override func viewDidLoad() {
super.viewDidLoad()
myButton.touchUpHandler = { [unowned self] in
self.performSomeAction()
}
}