GoFind Golf

As a first-time golfer, finding the right course should be an exciting adventure, not a hassle. With GoFind Golf, you can effortlessly discover the perfect golf club tailored just for you. Whether you're looking to refine your putting skills, warm up on the driving range, or savor a delicious club sandwich, our app has everything you need to enhance your golfing experience. Start your journey to the ideal round of golf today!

As a golf enthusiast, I faced a personal challenge in creating this app. I’ve always wanted to know if the golf course I’m about to visit has a putting green or driving range.

While it took me longer than I anticipated (about 20 days, including UI design) this journey was incredibly fulfilling. I was thrilled to code something that not only fulfilled my needs but also served a purpose.

Search.


import UIKit
import CoreLocation

class SearchVC: UIViewController, CLLocationManagerDelegate {
    
    let backgroundImageView = UIImageView()
    let darkTintView = UIView()
    let titleLabel = APSearchPageLabel(textAlignment: .center, fontSize: 60)
    let locationTextField = APTextField()
    let callToActionButton = APGoButton(backgroundColor: UIColor(red: 220/255, green: 185/255, blue: 0/255, alpha: 1.0), title: "GO!")
    let orImage = UIImageView()
    let useMyLocationButton = APUseMyLocationButton(backgroundColor: UIColor(red: 33/255, green: 58/255, blue: 46/255, alpha: 1.0), title: "Use My Location")
    
    var islocationEntered: Bool { return !locationTextField.text!.isEmpty }
    let locationManager = CLLocationManager()
    

    override func viewDidLoad() {
        super.viewDidLoad()
        
        configureTextField()
        configureCallToActionButton()
        configureOrImage()
        configureUseMyLocationButton()
        createDismissKeyboardTapGesture()
        layoutUI()

        locationManager.delegate = self
    }
    
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        configureBackgroundImageView()
        configureDarkTintView()
        
        titleLabel.text = "Find Your Perfect Place To Golf."
        
        navigationController?.setNavigationBarHidden(true, animated: true)
        
        locationTextField.text = ""
    }
    
    
    func configureBackgroundImageView() {
        
        backgroundImageView.image = Images.backgroundImage
        backgroundImageView.contentMode = .scaleAspectFill
    }
    
    
    func configureDarkTintView() { darkTintView.backgroundColor = UIColor.black.withAlphaComponent(0.4) }
    
    
    func configureCallToActionButton() { callToActionButton.addTarget(self, action: #selector(handleGoButton), for: .touchUpInside) }
    
    
    func configureTextField() { locationTextField.delegate = self }
    
    
    func configureOrImage() { orImage.image = Icons.orImage }
    

    func configureUseMyLocationButton() { useMyLocationButton.addTarget(self, action: #selector(handleUseMyLocation), for: .touchUpInside) }
    
    
    func layoutUI() {
        
        let assets = [backgroundImageView, darkTintView, titleLabel, callToActionButton, locationTextField, orImage, useMyLocationButton]
        
        for asset in assets {
            view.addSubview(asset)
            asset.translatesAutoresizingMaskIntoConstraints = false
        }


        NSLayoutConstraint.activate([
            backgroundImageView.topAnchor.constraint(equalTo: view.topAnchor, constant: -5),
            backgroundImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: -5),
            backgroundImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 5),
            backgroundImageView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 20),
            
            darkTintView.topAnchor.constraint(equalTo: view.topAnchor, constant: -5),
            darkTintView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: -5),
            darkTintView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 5),
            darkTintView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 20),
            
            titleLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 150),
            titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 50),
            titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -50),
            titleLabel.heightAnchor.constraint(equalToConstant: 290),
            
            callToActionButton.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 50),
            callToActionButton.leadingAnchor.constraint(equalTo: locationTextField.trailingAnchor, constant: -60),
            callToActionButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -45),
            callToActionButton.heightAnchor.constraint(equalToConstant: 60),
            
            locationTextField.topAnchor.constraint(equalTo: callToActionButton.topAnchor),
            locationTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 45),
            locationTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -120),
            locationTextField.heightAnchor.constraint(equalToConstant: 60),
            
            orImage.topAnchor.constraint(equalTo: locationTextField.bottomAnchor, constant: 25),
            orImage.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            
            useMyLocationButton.topAnchor.constraint(equalTo: orImage.bottomAnchor, constant: 25),
            useMyLocationButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 110),
            useMyLocationButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -110),
            useMyLocationButton.heightAnchor.constraint(equalToConstant: 60)
        ])
    }
    
    
    func createDismissKeyboardTapGesture() {
        
        let tap = UITapGestureRecognizer(target: view, action: #selector(UIView.endEditing))
        view.addGestureRecognizer(tap)
    }
    
    
    func pushGolfCourseListVC(destination: String? = nil, latitude: Double? = nil, longitude: Double? = nil) {
        
        if let destination = destination {
            DispatchQueue.global(qos: .userInitiated).async {
                NetworkManager.shared.getLongLat(destination: destination) { result in
                    switch result {
                    case .success(let location):
                        DispatchQueue.main.async {
                            self.navigateToGolfCourseListVC(latitudeString: location.lat, longitudeString: location.lon, destination: destination)
                        }
                    case .failure(let error):
                        print("Error fetching lat and long: \(error.rawValue)")
                        DispatchQueue.main.async {
                            self.presentAPAlertOnMainThread(title: "Location Error", message: "Failed to find the location you are looking for. Please check the spelling and try again.", buttonTitle: "OK")
                        }
                    }
                }
            }
        } else if let latitude = latitude, let longitude = longitude {
            DispatchQueue.main.async {
                let golfCourseListVC = GolfClubsListVC(location: "Current Location", latitude: latitude, longitude: longitude)
                self.navigationController?.pushViewController(golfCourseListVC, animated: true)
            }
        } else {
            DispatchQueue.main.async {
                self.presentAPAlertOnMainThread(title: "Error", message: "Please enter a location or use your current location.", buttonTitle: "OK")
            }
        }
        locationTextField.resignFirstResponder()
    }
    
    
    func navigateToGolfCourseListVC(latitudeString: String, longitudeString: String, destination: String) {
        guard let convertedLatitude = Double(latitudeString), let convertedLongitude = Double(longitudeString) else {
            print("Invalid latitude or longitude")
            return
        }

        let golfCourseListVC = GolfClubsListVC(location: destination, latitude: convertedLatitude, longitude: convertedLongitude)
        DispatchQueue.main.async {
            self.navigationController?.pushViewController(golfCourseListVC, animated: true)
        }
    }
    
    
    @objc func handleGoButton() {
        
        guard let destination = locationTextField.text, !destination.isEmpty else {
            presentAPAlertOnMainThread(title: "Empty Location", message: "Please enter a location to search.", buttonTitle: "OK")
            return
        }
        
        print("Destination entered: \(destination)")  // Debug print
        pushGolfCourseListVC(destination: destination)
    }
    
    
    @objc func handleUseMyLocation() {
        
        switch locationManager.authorizationStatus {
        case .notDetermined:
            locationManager.requestWhenInUseAuthorization()
        case .restricted, .denied:
            presentAPAlertOnMainThread(title: "Location Services Disabled", message: "Please enable location services in Settings.", buttonTitle: "OK")
        case .authorizedWhenInUse, .authorizedAlways:
            locationManager.requestLocation()
        @unknown default:
            fatalError("Unhandled case for location authorization status")
        }
    }
    
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        
        guard let location = locations.last else { return }
        let latitude = location.coordinate.latitude
        let longitude = location.coordinate.longitude
        pushGolfCourseListVC(latitude: latitude, longitude: longitude)
    }
    
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        presentAPAlertOnMainThread(title: "Location Error", message: "Failed to get your current location. Please try again.", buttonTitle: "OK")
    }
}


extension SearchVC: UITextFieldDelegate {
    
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        pushGolfCourseListVC()
        return true
    }
}

List.


import UIKit

class GolfClubsListVC: APDataLoadingVC {
    
    enum Section { case main }
    
    var location: String!
    var userLatitude: Double!
    var userLongitude: Double!
    
    var golfClubs: [GolfClub] = []
    var filteredGolfClubs: [GolfClub] = []
    
    var isSearching = false
    
    var collectionView: UICollectionView!
    var dataSource: UICollectionViewDiffableDataSource!
    
    
    init(location: String, latitude: Double, longitude: Double) {
        super.init(nibName: nil, bundle: nil)
        
        self.location = location
        self.userLatitude = latitude
        self.userLongitude = longitude
    }
    
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        configureViewController()
        configureSearchController()
        configureCollectionView()
        displayGolfClubs(longitude: userLongitude, latitude: userLatitude)
        configureDataSource()
        
    }
    
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        title = "Finding courses..."
        navigationController?.setNavigationBarHidden(false, animated: true)
    }
    
    
    func configureViewController() {
        
        view.backgroundColor = .white
        navigationController?.navigationBar.prefersLargeTitles = true
    }
    
    
    func configureCollectionView() {
        
        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: UIHelper.createRowFlowLayout(in: view, itemHeight: 250))
        view.addSubview(collectionView)
        collectionView.delegate = self
        collectionView.register(GolfClubCell.self, forCellWithReuseIdentifier: GolfClubCell.reuseID)
    }
    
    
    func configureSearchController() {
        
        let searchController = UISearchController()
        searchController.searchResultsUpdater = self
        searchController.searchBar.placeholder = "Search for golf club..."
        searchController.obscuresBackgroundDuringPresentation = false
        navigationItem.searchController = searchController
    }
    
    // Fetching GolfClub Model
    func displayGolfClubs(longitude: Double, latitude: Double) {
        
        showLoadingView()
        
        NetworkManager.shared.getGolfClubs(latitude: latitude, longitude: longitude, miles: 50) { [weak self] result in
            guard let self = self else { return }
            
            switch result {
            case .success(let golfClubs):
                
                self.updateUI(with: golfClubs)
                self.dismissLoadingView()
                
            case .failure(let error):
                self.presentAPAlertOnMainThread(title: "Was not able to retrieve golf club list", message: error.rawValue, buttonTitle: "OK")
            }
        }
    }
    
    
    func updateUI(with golfClubs: [GolfClub]) {
        
        self.golfClubs.append(contentsOf: golfClubs)
        self.updateData(on: self.golfClubs)
    }
    
    
    func configureDataSource() {
        
        dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView, cellProvider: { collectionView, indexPath, golfClub in
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: GolfClubCell.reuseID, for: indexPath) as! GolfClubCell
            cell.set(golfClub: golfClub, latitude: self.userLatitude, longitude: self.userLongitude)
            
            cell.imageIndex = indexPath.item % 30
            return cell
        })
    }
    
    
    func updateData(on golfClubs: [GolfClub]) {
        
        var snapshot = NSDiffableDataSourceSnapshot()
        snapshot.appendSections([.main])
        snapshot.appendItems(golfClubs)
        DispatchQueue.main.async { self.dataSource.apply(snapshot, animatingDifferences: true) }
    }
}


extension GolfClubsListVC: UICollectionViewDelegate {
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        
        let activeArray = isSearching ? filteredGolfClubs : golfClubs
        let golfClub = activeArray[indexPath.item]

        // network api call then transitin to next page
        let courseInfoVC = CourseInfoVC(golfClub: golfClub)
        DispatchQueue.main.async {
            self.navigationController?.pushViewController(courseInfoVC, animated: true)
        }
    }
}


extension GolfClubsListVC: UISearchResultsUpdating {
    
    func updateSearchResults(for searchController: UISearchController) {
        
        guard let filter = searchController.searchBar.text, !filter.isEmpty else {
            filteredGolfClubs.removeAll()
            updateData(on: golfClubs)
            isSearching = false
            return
        }

        isSearching = true
        filteredGolfClubs = golfClubs.filter { $0.clubName.lowercased().contains(filter.lowercased()) }
        updateData(on: filteredGolfClubs)
    }
}

Info.


import UIKit

protocol CourseInfoDelegate: AnyObject {  
}
class CourseInfoVC: APDataLoadingVC {
    
    let scrollView = UIScrollView()
    let contentView = UIView()
    let slideGallery = UIView()
    let generalDesc = UIView()
    let featureButtonsBox = UIView()
    let amenities = UIView()
    var itemViews: [UIView] = []
    
    let fieldDescriptionView = UIView()
    
    var golfClub: GolfClub!
    var placeID: String!
    
    weak var delegate: CourseInfoDelegate?
    
    init(golfClub: GolfClub) {
        super.init(nibName: nil, bundle: nil)
        
        self.golfClub = golfClub
        self.placeID = golfClub.placeId
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        configureScrollView()
        layoutUI()
        getCourseDetails()
        configureGolfClubUIElements(with: golfClub)
        configureFieldDescriptionView()
    }
    
    func configureFieldDescriptionView() {
        view.addSubview(fieldDescriptionView)
        
        fieldDescriptionView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            fieldDescriptionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            fieldDescriptionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            fieldDescriptionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            fieldDescriptionView.heightAnchor.constraint(equalToConstant: 220)
        ])
    }
    
    func configureScrollView() {
        
        view.addSubview(scrollView)
        scrollView.addSubview(contentView)
        
        scrollView.pinToEdges(of: view)
        contentView.pinToEdges(of: scrollView)
        scrollView.backgroundColor = .systemBackground
        
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        contentView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            contentView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: -150),
            contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
            contentView.heightAnchor.constraint(equalToConstant: 1250)
        ])
    }
  
        func getCourseDetails() {
        NetworkManager.shared.getClubInfo(placeId: placeID) { [weak self] result in
            guard let self = self else { return }
            
            switch result {
            case .success(let course):
                DispatchQueue.main.async { self.configureClubUIElements(with: course) }
                
            case .failure(let error):
                self.presentAPAlertOnMainThread(title: "Something went wrong sekki!", message: error.rawValue, buttonTitle: "OK")
            }
        }
    }
    
    func configureGolfClubUIElements(with golfClub: GolfClub) {
        self.add(childVC: APAmenitiesItemVC(club: golfClub), to: amenities)
        self.add(childVC: APFieldDescriptionsVC(club: golfClub), to: fieldDescriptionView)
    }
    
    func configureClubUIElements(with course: ClubInfo) {
        self.add(childVC: APCourseInfoImagesVC(club: course), to: slideGallery)
        self.add(childVC: APClubDescItemVC(club: course), to: generalDesc)
        self.add(childVC: APFeatureButtonsVC(club: course), to: featureButtonsBox)
    }
    
    private func layoutUI() {
        itemViews = [slideGallery, generalDesc, featureButtonsBox, amenities]
        
        for itemView in itemViews {
            contentView.addSubview(itemView)
            itemView.translatesAutoresizingMaskIntoConstraints = false
        }
                
        NSLayoutConstraint.activate([
            slideGallery.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
            slideGallery.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10),
            slideGallery.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10),
            slideGallery.heightAnchor.constraint(equalToConstant: 250),
            
            generalDesc.topAnchor.constraint(equalTo: slideGallery.bottomAnchor, constant: 10),
            generalDesc.leadingAnchor.constraint(equalTo: slideGallery.leadingAnchor, constant: 20),
            generalDesc.trailingAnchor.constraint(equalTo: slideGallery.trailingAnchor, constant: -20),
            generalDesc.heightAnchor.constraint(equalToConstant: 120),
            
            featureButtonsBox.topAnchor.constraint(equalTo: generalDesc.bottomAnchor, constant: 15),
            featureButtonsBox.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            featureButtonsBox.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            featureButtonsBox.heightAnchor.constraint(equalToConstant: 50),

            amenities.topAnchor.constraint(equalTo: featureButtonsBox.bottomAnchor, constant: 20),
            amenities.leadingAnchor.constraint(equalTo: generalDesc.leadingAnchor),
            amenities.trailingAnchor.constraint(equalTo: generalDesc.trailingAnchor),
            amenities.heightAnchor.constraint(equalToConstant: 600)
        ])
    }
    
    func add(childVC: UIViewController, to containerView: UIView) {
        addChild(childVC)
        containerView.addSubview(childVC.view)
        childVC.view.frame = containerView.bounds
        childVC.didMove(toParent: self)
    }
}