stefanjaindl

The world is a book and those who do not travel read only a page

abenteuer

Country Overview

Travelling is life and life is travelling

Skills

Personal Statement

Welcome on my personal Website! Here you can find my resume, an overview over my personal projects, helpful blog posts about diverse topics from a software developer's everyday work and impressions about my travels around the world. Enjoy :)

Travel Companion

Travel Companion is an iOS app I developed from 2018 - 2019 inspired by my passion for travelling, which is available on the Apple Store. Travel Companion's goal is to let explore, plan and remember travels. It has an integrated sign-in mechanism with either Facebook, Google, Apple, or email. This allows data synchronization over all your different devices. Here's a short demo video of the app: Within the app CoreData and Firebase Cloud Firestore are used for data persistence as well as various API's (Google Maps, Google Places, Google Geocoding API, Firebase Authentication, Firebase Storage, Firebase Analytics, Firebase Crashlytics, flickR API, Rome2Rio and restcountries.eu). Explore: The explore feature offers a Google Map that allows to search for desired destinations. Basic information about the destination and country as well as photos about the country, place and geographical location can be retrieved. Information provided by Wikipedia, Wikivoyage, Google and LonelyPlanet about the destination are offered, too. Plan: The plan feature allows to add travel plans for destinations that you have previously explored. The travel plans include flights, public transport (including ferries), hotels, restaurants and attractions. Flight and public transport search allows to enter an origin and destination with autocompletion support and a travel date. Available transport opportunities with detailed information are displayed in a TableView and can be added to the trip. Hotels, restaurants and attractions can be searched by destination: You can drop a pin on the map, which triggers a search for the chosen place type and distance. The search can further be restricted by text search. Attractions are subdivided in various groups offered by Google Places, that can be chosen in a picker (e.g. amusement parks, zoos, general points of interest, etc.). The plan overview displays all flights, public transports, hotels, restaurants and attractions in a grouped TableView. By tapping an item it is possible to delete it or to add a note. Furthermore, an image from the linked explore photos can be chosen by tapping the placeholder image. Finally, plans are subdivided into upcoming and past trips, depending on the travel date. Remember: The remember feature displays past trips (or trips that have been started), where you can add photos from the gallery or camera to remember your trip. Details can be checked on Travel Companion's homepage. Of course, I'd be pleased about downloads and reviews on the App Store.

Starting Eleven

A friend of me are passionate football players, and we play both at the hobby football club HV TDP Stainz. Out of that passion the idea of a comunio-like football manager website for hobby leagues was born. I have created that Single Page Web App written in Angular called "Football Manager". We are using the website club-internally - and it makes much fun to have our own Football Manager :) It supports the following features: OAuth authentication flow via Facebook, Google and E-Mail Possibility to create and join leagues Transfer market to buy and sell players (values based on player performance/points) Scoring system Live table Profile page Admin Area for scoring and statistics The app uses Firebase Cloud Firestore as database and communication via REST API to the backend. If you want to take a closer look or even start playing you can check out the website here.

Virtual Tourist

Virtual Tourist is an iOS app written in Swift, that allows users to virtually travel around the world, pinning locations, placing images and persisting their data in a local database. The app uses CoreData for data persistance and the flickR API as a REST-endpoint to retrieve the images based on location. The source code can be checked out here.

OnTheMap

OnTheMap is an iOS app written in Swift. It allows users to search for a location using geocoding, retrieve locations of other users via Parse API and share their location and a custom link. The source code can be checked out here.

Data Structures & Algorithms Collection Part 1: Arrays

Welcome to my series of "Data Structures & Algorithms Collection". For each blog post I'll walk through a sample problem and explain how to solve it. This blog deals with arrays. Zero Matrix: Write an algorithm such that if an element in an MxN matrix is 0, its entire row and column are set to 0. Suppose we have this matrix: [[1, 2, 0], [4, 5, 6], [7, 8, 0], [9, 10, 11]] It should be set to (nullifying all eligible rows and columns with 0 - this is row 1 & 3 + column 3): [[0, 0, 0], [4, 5, 0], [0, 0, 0], [9, 10, 0]] A first approach might be to set all columns and rows to zero on the fly, while iterating through all rows and columns. This doesn't work because it sets cells to zero we haven't checked yet. Soon the whole matrix may consist of zeroes. As a workaround we could define a second matrix and flagging rows & columns while iterating through the input matrix. This works, but needs O(M * N) extra space for the matrix. We can do better. We could use the first row and first column as temporary data store for our zero flags. Then, we first need to check whether the first row or column contains zeroes and memorize the result as boolean value (as we are destroying these cells - but so we are able to restore them). Then we iterate through the matrix starting from column and row indices 1. If a cell is zero, the corresponding index 0 row and columns are marked with 0, too. With that information in row & column index 0, we can then nullify all other cells and, finally, nullify row and column 0, if there initially was some zero value. This approach needs no extra space, so it is constant O(1) space and has the best conceivable runtime of O(M * N). Here's the source code: func zeroMatrix(matrix: inout [[Int]]) { guard !matrix.isEmpty, !matrix[0].isEmpty else { return } //We use the first row and column to zero the matrix in place //So, first remember whether we have to nullify first row and column let nullifyFirstRow = checkNullifyFirstRow(matrix: matrix) let nullifyFirstColumn = checkNullifyFirstColumn(matrix: matrix) //Check remaining cols/rows for row in 1 ..< matrix.count { for column in 1 ..< matrix[0].count { if matrix[row][column] == 0 { matrix[0][column] = 0 matrix[row][0] = 0 } } } //Nullify remaining columns for column in 1 ..< matrix[0].count { if matrix[0][column] == 0 { for row in 1 ..< matrix.count { matrix[row][column] = 0 } } } //Nullify remaining rows for row in 1 ..< matrix.count { if matrix[row][0] == 0 { for column in 1 ..< matrix[0].count { matrix[row][column] = 0 } } } if nullifyFirstRow { for column in 0 ..< matrix[0].count { matrix[0][column] = 0 } } if nullifyFirstColumn { for row in 0 ..< matrix.count { matrix[row][0] = 0 } } } private func checkNullifyFirstRow(matrix: [[Int]]) -> Bool { for col in 0 ..< matrix[0].count { if matrix[0][col] == 0 { return true } } return false } private func checkNullifyFirstColumn(matrix: [[Int]]) -> Bool { for row in 0 ..< matrix.count { if matrix[row][0] == 0 { return true } } return false } You can also check it out in my repository: ZeroMatrix

Data Structures & Algorithms Collection Part 2: Linked Lists

This is the second post of my "Data Structures & Algorithms Collection" series. In this blog post I'll walk through a sample problem with linked lists and explain how to solve it. Palindrome Checker: Implement a function to check if a linked list is a palindrome. What is a palindrome? It is simply a list that's read the same from front to back and back to front. For example: 1 -> 2 -> 3 -> 4 -> 3 -> 2 -> 1 It could have an even or odd count. One approach is to just create a copy of the linked list in reversed order. Then we can iterate through both linked lists step by step and compare the elements. All elements need to be equal, else it's no palindrome. After reaching the midpoint the iteration can be aborted, as all elements are checked from either original or reversed list - it is a palindrome. This approach needs linear extra space for the reversed linked list. Another approach is to move forward to the midpoint of the linked list (taking into account whether the list has an odd or even count) and pushing each element on a stack. Then continue moving forward, popping the respective element from the stack and comparing it to the current element. This works because stacks store their element in FIFO order. (Hint: if we know the linked list size we can just count until we reach the midpoint. If we don't know the size we can use a fast-slow-runner-technique. Moving with the slow pointer 1 node and with the fast runner 2 nodes forward. When the fast runner reaches the end, the slow runner will be at the midpoint.). This can also be done in a more elegant, recursive way. Our base case is when we reach the midpoint. From there on we return the next node (this is node + 1, + 2,... from midpoint), which is compared to the node the function call returns to (this is node - 1, - 2,... from midpoint). Actually we need to return values: The next node to compare and a bool indicating whether the palindrome check was successful previously - this can be done in a tuple in Swift or a wrapper class. Here's the source code: func isPalindrome(linkedList: SingleLinkedList<T>) -> Bool { guard let head = linkedList.head, linkedList.count > 1 else { return true } return isPalindrome(node: head, index: 0, count: linkedList.count).isPalindrome } private func isPalindrome(node: SingleNode<T>?, index: Int, count: Int) -> ResultWrapper<T> { if index >= Int(trunc(Double(count) / 2)) { let node = count % 2 == 0 /* even */ ? node : /* odd */ node?.next return ResultWrapper(node: node, isPalindrome: true) } var resultSoFar = isPalindrome(node: node?.next, index: index + 1, count: count) let equal = resultSoFar.node?.val == node?.val resultSoFar.isPalindrome = resultSoFar.isPalindrome && equal resultSoFar.node = resultSoFar.node?.next return resultSoFar } private struct ResultWrapper<T: Comparable> { var node: SingleNode<T>? var isPalindrome: Bool } You can also check it out in my repository: PalindromeChecker

Data Structures & Algorithms Collection Part 3: Stacks & Queues

This is the third post of my "Data Structures & Algorithms Collection" series. In this blog post I'll walk through a sample problem that deals with stacks and queues and explain how to solve it. Queue via Stacks: Implement a QueueWithStacks class which implements a queue using 2 stacks. The main difference between a queue and stack is in what order elements are removed. Stack is a LIFO data structure, where the last element added is the first being removed again. Vice versa, Queue is a FIFO data structure, where the first element added is the first being removed again. How can we take advantage of that? Well, we can always push elements on the first stack. When we want to pop an element we have to reverse the order. We can do that by popping elements from the first stack and pushing onto the second stack. The top element on the second stack will be the searched one. The performance can be improved by a lazy approach. Push operations always push on the first stack. Pop operations are then doing all the work. If the second stack is not empty we can just return the element. Elsewise we pop all elements from stack one and push onto stack 2. The amortized time for that is O(1). Here's the source code: class QueueWithStacks<T: Comparable> { private var firstStack = Stack<T>() private var secondStack = Stack<T>() open func enqueue(value: T) throws { firstStack.push(val: value) } open func dequeue() throws -> T { if firstStack.isEmpty() && secondStack.isEmpty() { throw NSError(domain: "Stack: Invalid call", code: 0, userInfo: nil) } moveElements(from: firstStack, to: secondStack) return try secondStack.pop() } open func isEmpty() -> Bool { return firstStack.isEmpty() && secondStack.isEmpty() } private func moveElements(from: Stack<T>, to: Stack<T>) { while !from.isEmpty(), let node = try? from.pop() { to.push(val: node) } } } You can also check it out in my repository: QueueWithStacks

Feature Flag Architecture

I have once implemented a complete feature flag architecture for an iOS app. Such an architecture is important to switch features on or off, even remotely so that no new build is required. This blog post describes how the architecture was implemented. First a general Feature protocol and an implementation of it is provided: protocol Feature { var key: String { get } } struct FeatureFlag: Feature { let key: String init(key: FeatureKey) { self.key = key.rawValue } } Then, we define an enum FeatureKey, which represent the unique features within the app: enum FeatureKey: String, CaseIterable { case firstFeature case superFeature case uniqueFeature ... } The FeatureFlagProviderType enum represents the different sources where feature flags come from. In this case it is debug, release, remote (for switching features on/off remotely) and test. enum FeatureFlagProviderType { case debug, release, remote, test } Next, the FeaturePriority struct defines feature orders, when more than one FeatureFlagProviderType is applicable to a feature: struct FeaturePriority { static let minPriority = 1 static let mediumPriority = 5 static let maxPriority = 10 static let testPriority = 15 } Then the protocol FeatureFlagProvider requires that priority as well as the type. Implementers must provide methods that determine whether it defines that feature, and if so, whether it is enabled: protocol FeatureFlagProvider: class { var priority: Int { get } var type: FeatureFlagProviderType { get } func isFeatureEnabled(_ featureKey: FeatureKey) -> Bool func hasFeature(_ featureKey: FeatureKey) -> Bool } The feature flag providers should be instantiated via a factory: final class FeatureFlagProviderFactory { static func createProvider(type: FeatureFlagProviderType, with featurePairs: [FeaturePair] = [], userDefaults: UserDefaults? = nil, remoteSync: Remote? = nil) -> FeatureFlagProvider { switch type { case .debug: return DebugFeatureFlagProvider(userDefaults: userDefaults ?? .standard) case .release: return ReleaseFeatureFlagProvider() case .remote: return ContentfulFeatureFlagProvider(featurePairs: featurePairs, remoteSync: remoteSync) case .test: return TestFeatureFlagProvider() } } } We will look in detail at the debug and release providers. The ReleaseFeatureFlagProvider has min priority, so that it can be overruled by remote flags. In this implementation it provides valus for all features, whether they are generally enabled or not: //Used in release version final class ReleaseFeatureFlagProvider: FeatureFlagProvider { internal let priority = FeaturePriority.minPriority internal let type = FeatureFlagProviderType.release func isFeatureEnabled(_ featureKey: FeatureKey) -> Bool { switch featureKey { case firstFeature: return true case superFeature: return false case uniqueFeature return true } } func hasFeature(_ featureKey: FeatureKey) -> Bool { switch featureKey { case firstFeature: return true case superFeature: return true case uniqueFeature return true } } } The DebugFeatureFlagProvider should only be added for debug/dev builds. Features can be switched on or off by UserDefaults settings. It may be handy to provide a UI for doing so: final class DebugFeatureFlagProvider: MutatingFeatureFlagProvider { private let newFeaturesInitiallyEnabled = true private let userDefaults: UserDefaults internal let priority = FeaturePriority.mediumPriority internal let type = FeatureFlagProviderType.debug init(userDefaults: UserDefaults) { self.userDefaults = userDefaults } func isFeatureEnabled(_ featureKey: FeatureKey) -> Bool { switch featureKey { case .routing: return userDefaults.bool(forKey: featureKey.rawValue, defaultValue: false) default: return userDefaults.bool(forKey: featureKey.rawValue, defaultValue: newFeaturesInitiallyEnabled) } } func hasFeature(_ featureKey: FeatureKey) -> Bool { return true } func setFeatureEnabled(feature: Feature, enabled: Bool) { userDefaults.set(enabled, forKey: feature.key) } }

Overlay with Infinite Scrolling Behaviour

I have worked once on a ViewController that should support drag & drop of a child ViewController with a certain amount of fixed sheet states. I want to share how such a feature can be implemented. Your browser does not support the video tag. There are actually three ViewControllers involved: The outer ContainerViewController The inner ChildViewController Another ViewController that is embedded in the ChildViewController - let's call it EmbeddedViewController Let's deal with the ContainerViewController first. The ContainerViewController deals with the main logic. It adds and talks with the Child and Embedded ViewControllers with the help of pan gesture recognizers and delegates to get informed about the scrolling position of the content and adjusts the overlay accordingly. Adjustments happens on a top constraint from the top safe area to the ChildViewController. It also handles bouncing and keyboard show/hide events. Here is the code for the ContainerViewController: final class ContainerViewController: UIViewController { private var gesture: UIPanGestureRecognizer? private var currentState: SheetState = .sheetDefault private var currentTranslationOffset: CGFloat = 0.0 private var keyboardHeight: CGFloat = 0.0 private var initialOverlayAdjustmentDone = false override func viewDidLoad() { super.viewDidLoad() setupUI() addBottomSheet() setupGesture() addObservers() } func setupGesture() { gesture = UIPanGestureRecognizer(target: self, action: #selector(panGesture)) if let gesture = gesture { stackView.addGestureRecognizer(gesture) } } @objc func panGesture(recognizer: UIPanGestureRecognizer) { let dragVerticalLocation = recognizer.location(in: panelDragContainer).y if currentTranslationOffset == 0.0 { currentTranslationOffset = dragVerticalLocation } let adjustmentPosition = currentTranslationOffset - dragVerticalLocation handleInfiniteScroll(to: panelTopConstraint.constant - adjustmentPosition, animated: false, velocity: recognizer.velocity(in: panelDragContainer).y) if recognizer.state == .ended || recognizer.state == .cancelled { let state = viewModel.nearestState(for: panelTopConstraint.constant, currentState: currentState) currentState = state adjustOverlay(to: verticalPosition(for: state), animated: true) currentTranslationOffset = 0.0 containerOverlayDelegate?.didDragOverlay() } } private func addBottomSheet() { let embeddedPage = EmbeddedPageViewController.create( .. viewConfig: ViewConfig(customModalPresentationStyle: .overCurrentContext, ..) .. ) let childViewController =ChildViewController.create(rootViewController: embeddedPage, ..) childViewController.containerDelegate = self containerOverlayDelegate = childViewController embeddedPage.containerNavigationDelegate = childViewController let navigationController = childViewController.embedInNavigationController().withModalPresentationStyle(.fullScreen) embedd(controller: navigationController) } } extension ContainerViewController { private func handleInfiniteScroll(to y: CGFloat?, animated: Bool, velocity: CGFloat = 0.0) { let scrollDirection = SheetScrollDirection.directionForVelocity(velocity: velocity) //If we did not scroll at all (scrollDirection == .unchanged), we can stop immediately guard let y = y, scrollDirection != .unchanged, let containerOverlayDelegate = containerOverlayDelegate, let gesture = gesture else { return } //.. Otherwise we have to handle the overlay, and the content in the EmbeddedViewController if overlayIsAtTop() { //if the overlay is at its maximum sheet state position.. //Drag content: This handles movement of embedded content let contentIsAtTop = containerOverlayDelegate.dragContent(velocity: velocity * SheetStateConfig.panVelocityToTranslationFactor) if contentIsAtTop { //If overlay is at top (= embedded content is at the very top of the overlay): Adjust overlay itself adjustOverlay(to: y - keyboardHeight, animated: animated, scrollDirection: scrollDirection) } else { //Else content was scrolled/adjusted, but is not on top yet. Just remember currentTranslationOffset (we need that later for proper adjustment of the overlay) currentTranslationOffset = gesture.location(in: panelDragContainer).y } } else { //if the overlay is not at its maximum sheet state position, we just adjust the overlay adjustOverlay(to: y - keyboardHeight, animated: animated, scrollDirection: scrollDirection) } } private func adjustOverlay(to yPosition: CGFloat, animated: Bool, scrollDirection: SheetScrollDirection = .unchanged) { let constant = adjustOverlayPosition(for: yPosition) panelTopConstraint.constant = constant if animated { UIView.animate(withDuration: 0.1, delay: 0, options: .curveLinear, animations: { [weak self] in self?.view.layoutIfNeeded() }) } } private func adjustOverlayPosition(for yPosition: CGFloat) -> CGFloat { let minAllowedPos = verticalPosition(for: .sheetDefault) let maxAllowedPos = verticalPosition(for: .maximum) let adjustedOverlayPosition = max(yPosition, maxAllowedPos) return adjustedOverlayPosition > minAllowedPos ? minAllowedPos : adjustedOverlayPosition } private func overlayIsAtTop() -> Bool { return panelTopConstraint.constant <= verticalPosition(for: .maximum) } private func verticalPosition(for state: SheetState) -> CGFloat { return viewModel.verticalPosition(for: state) } func addObservers() { NotificationCenter.default.addObserver( self, selector: #selector(keyboardDidShow), name: UIResponder.keyboardDidShowNotification, object: nil ) NotificationCenter.default.addObserver( self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil ) } @objc func keyboardDidShow(_ notification: Notification) { if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue { let keyboardRectangle = keyboardFrame.cgRectValue keyboardHeight = keyboardRectangle.height } } @objc func keyboardWillHide(_ notification: Notification) { keyboardHeight = 0 } } Some of the logic can be factored out into a ContainerViewModel. The ContainerViewModel declares the available SheetStates in an enum and initializes their vertical positions in a map. It contains also the logic to search for the nearest state the overlay should be adjusted to, when a pan gesture ended: final class ContainerViewModel { enum SheetState: String, CaseIterable { case minimized case teaser case default case expanded case maximum } private var statePosition: Array<CGFloat> = Array<CGFloat>(repeating: 0, count: SheetState.allCases.count) private var innerItemOffset: CGFloat = 280.0 init(viewHeight: CGFloat) { setupSheetState(viewHeight: viewHeight) } func setupSheetState(viewHeight: CGFloat) { let minHeight = viewHeight - innerItemOffset statePosition[SheetState.sheetDefault.rawValue] = minHeight statePosition[SheetState.teaser.rawValue] = (minHeight + SheetStateConfig.maximumStatePosition) / 2 statePosition[SheetState.maximum.rawValue] = SheetStateConfig.maximumStatePosition } func verticalPosition(for state: SheetState) -> CGFloat { return statePosition[state.rawValue] } func nearestState(for verticalTranslation: CGFloat, currentState: SheetState) -> SheetState { var smallestDiff: CGFloat = CGFloat.greatestFiniteMagnitude var nextState: SheetState? = currentState for (state, position) in statePosition.enumerated() { let currentDiff = abs(verticalTranslation - position) if currentDiff < smallestDiff { smallestDiff = currentDiff nextState = SheetState(rawValue: state) } } return nextState ?? SheetState.sheetDefault } } The ChildViewController is responsible for scrolling/setting constraints of the inner content, menu & filters, and calling the delegate methods the ContainerViewController is using. final class ChildViewController: UIViewController { func setContainerOverlayDelegate() { if let containerViewController = navigationController?.parent as? ContainerViewController { containerViewController.containerOverlayDelegate = self } } extension ChildViewController: ContainerOverlayDelegate { func dragContent(velocity: CGFloat) -> Bool { guard let rootViewController = rootViewController as? EmbeddedViewController else { return true } //First, we always adjust the constraints of filter/menu, if necessary: var adjusted = adjustFilterAndNavigation(distanceFromTop: velocity) //If the content fits vertically on the screen, we just need to adjust filter/menu if rootViewController.fitsVerticallyOnScreen() { return !adjusted } //If there was no need to adjust the filter/menu constraints, we scroll the content: if !adjusted { rootViewController.scrollContent(with: velocity) //Also in the case after scrolling the content, we may need to adjust filter/menu constraints if rootViewController.isAtTop() { adjusted = adjustFilterAndNavigation(distanceFromTop: velocity) } } //If either menu/filter or content has changed, we scrolled something inside there return !adjusted && rootViewController.isAtTop() } func didDragMapOverlay() { (rootViewController as? EmbeddedViewController)?.bounce() } private func adjustFilterAndNavigation(distanceFromTop: CGFloat) -> Bool { let constant: CGFloat = filter.isEmpty ? Self.menuTopHeight : Self.filterTopHeight + (searchBar?.frame.size.height ?? 0) let scrollDirection = SheetScrollDirection.directionForVelocity(velocity: distanceFromTop) if scrollDirection == .down { guard searchBarTopConstraint.constant > -constant else { return false } searchBarTopConstraint.constant = max(-constant, searchBarTopConstraint.constant + distanceFromTop) return true } else { guard searchBarTopConstraint.constant < 0, distanceFromTop < constant else { return false } searchBarTopConstraint.constant = min(0, searchBarTopConstraint.constant + distanceFromTop) return true } } } Finally, the EmbeddedViewController reacts to scroll events from the ChildViewController: extension EmbeddedViewController { func scrollContent(with velocity: CGFloat) { guard scrollView != nil else { return } scrollView.contentOffset.y -= velocity } func bounce() { scrollView.bounceToMaxContentOffset() } func isAtTop() -> Bool { return scrollView.contentOffset.y <= scrollView.minContentOffset.y } func fitsVerticallyOnScreen() -> Bool { return scrollView.fitsVerticallyOnScreen() } } To encapsulate the scroll view specific actions, there is an extension to UIScrollView: extension UIScrollView { var minContentOffset: CGPoint { return CGPoint(x: -contentInset.left, y: -contentInset.top) } var maxContentOffset: CGPoint { return CGPoint(x: contentSize.width - bounds.width + contentInset.right, y: contentSize.height - bounds.height + contentInset.bottom + 16) } func bounceToMaxContentOffset() { if contentOffset.y > maxContentOffset.y + 16 { let rect = CGRect(x: contentOffset.x, y: maxContentOffset.y + 16, width: 1, height: 1) scrollRectToVisible(rect, animated: true) } } func fitsVerticallyOnScreen() -> Bool { return bounds.height + contentInset.bottom >= contentSize.height } }

UIKit Cheatsheet Part 4: UIView Roundings

This article is part 4 of my UIKit Cheatsheet series. It deals with roundings for all kinds of UIViews. Roundings on buttons, labels etc. are looking quite good. That's why we want to have it for our views in iOS. There are generally 2 preferrable ways of doing these roundings: CALayer extension CALayer { func roundCorners(_ radius: CGFloat, cornerMask: CACornerMask? = nil) { cornerRadius = radius if let cornerMask = cornerMask { maskedCorners = cornerMask } masksToBounds = true } } Using CALayer existing properties is an easy (and likely the most efficient) solution for simple corner masking. It's animatable, too, and the masked corners can be set (e.g. round all corners, only top corners etc.). CALayerMask public func round(corners: UIRectCorner, radius: CGFloat) { let maskPath = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius)) let maskLayer = CAShapeLayer() maskLayer.frame = bounds maskLayer.path = maskPath.cgPath layer.mask = maskLayer } CAShapeLayer masks are nice approach if the corner masking isn't simple corner rounding but some arbitrary path. You have to be cautious to make sure to update this mask if the frame changes (e.g. in layoutSubviews of view or in viewDidLayoutSubviews of controller). Animating requires more work. I'd suggest option 1 for simple cases, and option 2 if you need more control than option 1 can offer.

Theming

An important topic for an iOS app may be to use a consistent theming across the whole UI. This blog post shows a few options how to do app theming. Set the text color of DescriptiveLabel (subclass of UILabel) in a CustomCell: DescriptiveLabel.appearance(whenContainedInInstancesOf: [CustomCell.self]).textColor = textDescriptiveColor Setting state (normal, highlighted, disabled,..) coloring of a button: UIButton.appearance(whenContainedInInstancesOf: [CustomViewController.self]).setStateColors(themeColor: primary) The following UIButton extension is pretty handy for that: extension UIButton { func setStateColors(colorStates: [ColorState]) { colorStates.forEach { state in state.titleColor.flatMap { setTitleColor($0, for: state.buttonState) tintColor = .white backgroundColor = .white } state.backgroundColor.flatMap { setBackgroundColor($0, for: state.buttonState) } } } func setStateColors(themeColor: ThemeColor) { let states: [ColorState] = [ .normal(titleColor: themeColor.tintColor, backgroundColor: themeColor.fillColor), .highlighted(titleColor: themeColor.tintHighlightedColor, backgroundColor: themeColor.fillHighlightedColor), .selected(titleColor: themeColor.tintSelectedColor, backgroundColor: themeColor.fillSelectedColor) ] setStateColors(colorStates: states) } } Customize navigation appearance attributes: let titleAttributes: [NSAttributedString.Key: Any] = [ .foregroundColor: navigationBar.textColor ] let buttonAttributes: [NSAttributedString.Key: Any] = [ .foregroundColor: navigationBar.tintColor ] let navBar = UINavigationBar.appearance() let standardAppearance = UINavigationBarAppearance() standardAppearance.configureWithOpaqueBackground() standardAppearance.backgroundColor = navigationBar.backgroundColor standardAppearance.titleTextAttributes = titleAttributes standardAppearance.largeTitleTextAttributes = titleAttributes standardAppearance.shadowColor = seperatorColor standardAppearance.applyTitleAttributesForAllButtonAppearances(buttonAttributes) navBar.standardAppearance = standardAppearance let scrollEdgeAppearance = UINavigationBarAppearance() scrollEdgeAppearance.configureWithTransparentBackground() scrollEdgeAppearance.titleTextAttributes = titleAttributes scrollEdgeAppearance.largeTitleTextAttributes = titleAttributes scrollEdgeAppearance.applyTitleAttributesForAllButtonAppearances(buttonAttributes) navBar.scrollEdgeAppearance = scrollEdgeAppearance }

UIKit Cheatsheet Part 1: UINavigationBar

When working on iOS apps, an important topic for every developer is UIKit (and of course increasingly SwiftUI in the future). I have made a short Cheatsheet for the most important topics I came across quite often during iOS development with UIKit. This article deals with tricks for UINavigationBar. To prevent the navigation bar title to be displayed too large, which may not look good, largeTitleDisplayMode can be set: navigationItem.largeTitleDisplayMode = .never A navigation bar button item can be added as follows: navigationItem.rightBarButtonItem = UIBarButtonItem( title: "Navigation Item", style: .plain, target: self, action: #selector(didTapDismiss)) A navigation title view with customized font, text, colors, constraints etc., can be added as follows (this code sets a customized label, that is shown in the header image): private func setupNavigationTitleView() { let frame = navigationController?.navigationBar.frame ?? CGRect.zero let titleLabel = UILabel(frame: frame) titleLabel.text = title titleLabel.textAlignment = .center titleLabel.font = UIFont.systemFont(ofSize: 15, weight: .medium) titleLabel.textColor = themeColors.primary.tintColor titleLabel.backgroundColor = themeColors.primary.fillColor titleLabel.lineBreakMode = .byWordWrapping titleLabel.widthAnchor.constraint(equalToConstant: titleLabel.intrinsicContentSize.width + 48).isActive = true titleLabel.heightAnchor.constraint(equalToConstant: titleLabel.intrinsicContentSize.height + 12).isActive = true titleLabel.layer.roundCorners(16.0) navigationItem.titleView = titleLabel }

Dominikanische Republik

Im Land über und unter dem Winde

Abu Dhabi

Deutschland

Im Land der Curry-Würste

Niederlande

Im Land der Dämme

Laos

Geheimtipp aus Südostasien

Polen

Im Land der Burgen

Slowenien

Im Land der wundervollen Landschaften

USA

Im Land der unbegrenzten Möglichkeiten

Sidebar

Some Text Widget You can edit this by modifying the modules/sidebar/default.md page. To reorder things in the sidebar you need to modify the partials/sidebar.html.twig template file.

Education

Recognitions

Language Skills

No One Left Behind - create@school

From 2011 to 2018 I've worked actively for the Open Source project "Catrobat", where I also have written my Software engineering Bachelor's thesis as well as Master's thesis at Graz University of Technology. Mainly I have worked on the Android app ("Pocket Code"), which is available on Google Play for Education, but also partly on the iOS versions and on the website (based on Symfony). The vision of Catrobat is to develop solutions which inspire especially childs and teenagers to learn coding, get creative and be prepared for the digital world of our future. The founder of Catrobat, Prof. Wolfgang Slany, explains it here: You can find out more about Catrobat here. One of the coolest features I've implemented for Catrobat was the create@school project. It is a standalone Android app (actually a newly introduced gradle product flavor of "Pocket Code") which was especially targeted at educational environments. It was allowing 600+ children in five Austrian, Spanish and British schools to develop digital games on mobile devices linked to their school curriculum. Technically I've introduced flavouring with gradle to "Pocket Code" with the create@school app, allowing additions such as complex template projects downloaded via Web Service. You can find out more about this great project here.

Data Structures & Algorithms Collections

I had the pleasure to meet the famous Linus Torvalds, founder of Linux, at Google Summer of Code Reunion 2014 in San Jose, California. As he once said, "bad programmers worry about the code. Good programmers worry about data structures and their relationships." I totally agree with him and that's why I have created a collection of data structures and algorithms. The package is written in Swift. It includes well-known data structures and algorithms such as Djikstra's shortest path algorithm, as well as algorithms for specific problems such as checking whether a string contains all unique characters. Each data structures and algorithm class also has a corresponding unit test with, if applicable, a link to test data/further decription. The package is available on Swift Package Manager and the repository can be checked out on my github page: https://github.com/sjaindl/DataStructuresAlgs. Besides various basic as well as advanced data structures, the package offers algorithms of the following areas: Arrays & Strings Linked Lists Stacks & Queues Trees & Graphs Bit Manipulation Recursion and Dynamic Programming Sorting & Searching Threading Algorithms are based on the questions of the book "Cracking the Coding Interview" from Gayle Laakmann McDowell. I also have an algorithms collection written in Phyton, that can be checked out here.

MemeMe

MemeMe is an iOS app written in Swift. It is a funny photo editor that allows users to create and share Memes with friends. Photos can be added either from an album or from the camera and customized with a top and a bottom text. The resulting Meme can then be shared or saved. It can also be reviewed later in a Meme overview (the overview can be switched between a TableView and a CollectionView) as well as in detail when tapping a Meme in the overview. The source code can be checked out here.

Data Structures & Algorithms Collection Part 5: Bit Manipulation

This is the fifth post of my "Data Structures & Algorithms Collection" series. In this blog post I'll walk through a sample bit manipulation problem. Given a real number between 0 and 1 (e.g. 0.77) that is passed in as a double, output the binary representation. If the number cannot be printed accurately with 32 characters, an error should be thrown. How are real binary numbers represented? 1 / 2^1 = 0.5 1 / 2^2 = 0.25 1 / 2^3 = 0.125 ... By knowing that, one way is to repeatedly shift the number left by 1 and check whether the number is > 1 - in this case the MSB (most significant bit) was set. We can append 1 to the output and subtract 1, else we just append 0 to the result. Here's the source code: class BinaryToStringConverter { private let errorString = "ERROR" func binaryRepresentation(of number: Double) -> String { var binary = "." var currentNumber = number if currentNumber <= 0 || currentNumber >= 1 { return errorString } while currentNumber > 0 { if binary.count >= 32 { return errorString } let digit = currentNumber * 2 //Same as shift left by 1 binary.append(digit >= 1 ? "1" : "0") currentNumber = digit >= 1 ? digit - 1 : digit } return binary } } You can also check it out in my repository: Binary To String Converter

Data Structures & Algorithms Collection Part 6: Recursion

This is the 6. post of my "Data Structures & Algorithms Collection" blog series. This blog deals with a recursion problem using dynamic programming. Stack of Boxes: You have a stock of n boxes, with widths wi, heights hi and depths di. The boxes cannot be rotated and can only be stacked on top of each other if each box in the stack is strictly larger than the box above it in width, height and depth. Implement a method to compute the height of the tallest possible stack. The height of a stack is the sum of the heights of each box. The bruteforce solution to this problem obviously would be to just try all valid combinations of all boxes. This would result in a very bad runtime - we can do better. The problem states that each box must be larger than the previous one in terms of width, height and depth. We can use that property and sort by any property - e.g. by height first in O(n log n) time. We know that the final result must be in the resulting order (probably not including all boxes). We effectively eliminated the height dimension with this initial sort. Now we need to find the largest stack by trying all stack combinations that can be formed by this ordering. We can start including the first box, then starting from the second box, and so on. Of course we need to check for each step if it is a valid combination also using width and depth. We don't need to check the height, as we already have sorted by height. This results in still at least O(n * n!) runtime, as there are n! permutations with a recurive call stack of depth n each. We can still optimize it further by using dynamic programming. We can declare a hashmap which basically caches the largest result found at each stack index. We pass that hashmap down the recursive call stack and can use it for valid indices. Alternatively, if the box is represented with a class, a maxHeight property can be added to the class. Here's the source code: class StackOfBoxes { func maxHeight(of stack: [Box]) -> Int { if stack.isEmpty { return 0 } let sorted = stack.sorted(by: >) var maxHeight = 0 for (index, box) in sorted.enumerated() { if box.height > maxHeight { maxHeight = box.height } for previous in 0 ..< index { if isValid(first: box, second: sorted[previous]) && sorted[previous].maxHeight + box.height > box.maxHeight { box.maxHeight = box.height + sorted[previous].maxHeight maxHeight = max(maxHeight, box.maxHeight) } } } return maxHeight } private func isValid(first: Box, second: Box) -> Bool { return first.width < second.width && first.height < second.height && first.depth < second.depth } } class Box: Equatable, Comparable { public let width: Int public let height: Int public let depth: Int public var maxHeight: Int public init(width: Int, height: Int, depth: Int) { self.width = width self.height = height self.depth = depth maxHeight = height //max height together with other boxes is at least == height of the box itself } public static func < (lhs: Box, rhs: Box) -> Bool { return lhs.height < rhs.height } public static func == (lhs: Box, rhs: Box) -> Bool { return lhs.width == rhs.width && lhs.height == rhs.height && lhs.depth == rhs.depth } } You can also check it out in my repository: StackOfBoxes

JSON objects in CoreData

This blog post describes how JSON objects can be stored, serialized and deserialized using CoreData. CoreData offers the possibility to store entries of type "Transformable": This is pretty convenient to store custom classes or even JSON data that comes e.g. from an API call. This entry can be stored as NSDictionary, serialized and deserialized as follows: public final class MyCoreDataEntry { @NSManaged public internal(set) var values: NSDictionary … public func decodeValuePairs() -> [ValuePair] { let valuePairs: [ValuePair] = features.compactMap { element in guard let value = element.key as? String, !value.isEmpty, let enabled = element.value as? Bool else { return nil } return ValuePair(key: value, enabled: enabled) } return valuePairs } public struct ValuePair: Equatable { public let key: String public let enabled: Bool public static func == (lhs: ValuePair, rhs: ValuePair) -> Bool { return lhs.key == rhs.key && lhs.enabled == rhs.enabled } } }

Google Maps Overlay & Two-Finger-Scrolling

Google Maps is a very much-used library on Android. This blog post demonstrates how a Two-Finger-Scroll can be implemented. Two-Finger scrolling and hint to use 2 fingers: Your browser does not support the video tag. First we define an interface to show hints: interface MapScrollInterceptor { fun showHint() } The core logic lies in the custom MapView class that is designed to react on double finger movements only. Movements with one finger only are intercepted with requestDisallowInterceptTouchEvent. The pointerCount indicates how many fingers are tapping the MapView: class DoubleTouchableMapView : MapView { constructor(context: Context) : super(context) constructor(context: Context, options: GoogleMapOptions) : super(context, options) var interceptorCallback: MapScrollInterceptor? = null override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { val touchEvent = ev ?: return false parent.requestDisallowInterceptTouchEvent(touchEvent.pointerCount > 1) return super.dispatchTouchEvent(ev) } override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { val touchEvent = ev ?: return false if (touchEvent.action == MotionEvent.ACTION_MOVE) { if (touchEvent.pointerCount < 2) { //Intercept move with one finger only interceptorCallback?.showHint() } return touchEvent.pointerCount < 2 } return super.onInterceptTouchEvent(ev) } } The GoogleMapsFragment implements the MapScrollInterceptor interface and contains the MapView. It shows (and hides again) the hint using animations: class GoogleMapsFragment : Fragment(), MapScrollInterceptor { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setupMap() mapView.onCreate(savedInstanceState) mapView.interceptorCallback = this } override fun showHint() { if (isAnimating) { return } isAnimating = true map_scroll_hint.animate() .setStartDelay(0L) .setDuration(250L) .alpha(0.8f) .setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator?) { super.onAnimationEnd(animation) hideHint() } }) .start() } private fun hideHint() { map_scroll_hint.animate() .setStartDelay(2000L) .setDuration(250L) .alpha(0.0f) .setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator?) { super.onAnimationEnd(animation) isAnimating = false } }) .start() } The layout of GoogleMapsFragment looks the following: <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <FrameLayout android:id="@+id/map_view_container" android:layout_width="match_parent" android:layout_height="match_parent" /> <TextView android:id="@+id/map_scroll_hint" android:layout_width="match_parent" android:layout_height="50dp" android:alpha="0.0" android:background="@android:color/darker_gray" android:gravity="center" android:text="@string/use_two_fingers_to_scroll_the_map" android:textColor="@android:color/white" android:textSize="@dimen/textSizeButton" app:layout_constraintBottom_toBottomOf="@+id/map_view_container" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout>

UIKit Cheatsheet Part 7: Loading

This article is part 7 of my UIKit Cheatsheet series. It deals with loading indicators. Loading indicators are important to show to users that something is happening (e.g. data is fetched from network). In this blog post I describe 2 methods of doing so. Show loading indicator on a specific view, e.g. a button. Therefore a UIActivityIndicatorView needs to be connected from storyboard (or created in code) and the loading state needs to be set before a network call and reset after it finished: @IBOutlet var activityIndicator: UIActivityIndicatorView! activityIndicator.isLoading = true imageView.kf.setImage(with: url.imageWith(preset: .content), placeholder: nil, options: nil, progressBlock: nil) { [weak self] result in self?.activityIndicator.isLoading = false I'm using the following handy extension to set the loading indicator: extension UIActivityIndicatorView { var isLoading: Bool { get { return isAnimating } set { if newValue { isHidden = false startAnimating() } else { stopAnimating() } } } } Displaying loading animation as an entire view: private lazy var loadingView = CustomLoadingView.Builder() .with(backgroundColor: themeColors.defaultBackground.fillColor) .with(title: L10n.loadingTicketInformation) .withLoadingSpinner() .build() ... if viewModel.initialLoading { loadingView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(loadingView) loadingView.bindToSuperView(edgeInsets: .zero)} else { loadingView.removeFromSuperview() }   In my example I'm using a CustomLoadingView. You can actually use any view you like, you just need to pin it to the superview, so that it takes up the entire screen.

Scanning codes in iOS

The AVFoundation framework offers a pretty good way to scan codes with the device camera, such as qr codes or barcodes. This blog post gives an implementation of doing such code scanning. Subsequently is the implementation of this feature. It also offers the possibility to toggle torch light on and off to facility scanning. After the view is loaded a captureSession is obtained and video input is locked. If some code is recognized, AVFoundations tells us so by calling the delegate methods from AVCaptureMetadataOutputObjectsDelegate. The metadataObjectTypes property of AVCaptureMetadataOutput allows us to filter for different codes, such as qr codes or bar codes. final class ScanCodeViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate { override func viewDidLoad() { super.viewDidLoad() scan() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) if captureSession?.isRunning == false { captureSession?.startRunning() } } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) if captureSession?.isRunning == true { captureSession?.stopRunning() } } private func setupNavigation() { if let device = AVCaptureDevice.default(for: .video), device.hasTorch { createTorchBarItem(with: Asset.Images.flashOn.image) } } private func createTorchBarItem(with image: ImageAsset.Image) { navigationItem.leftBarButtonItem = UIBarButtonItem.buttonItem( image: image, target: self, action: #selector(toggleTorch)) } private func scan() { captureSession = AVCaptureSession() guard let videoCaptureDevice = AVCaptureDevice.default(for: .video), let captureSession = captureSession else { logWarning("Could not retrieve capture session", tag: Self.tag) return } let videoInput: AVCaptureDeviceInput do { videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice) } catch { logWarning("Could not capture video input", tag: Self.tag) return } if (captureSession.canAddInput(videoInput)) { captureSession.addInput(videoInput) } else { failed() return } let metadataOutput = AVCaptureMetadataOutput() if (captureSession.canAddOutput(metadataOutput)) { captureSession.addOutput(metadataOutput) metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) metadataOutput.metadataObjectTypes = [.code128] } else { failed() return } let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) previewLayer.frame = view.layer.bounds previewLayer.videoGravity = .resizeAspectFill view.layer.addSublayer(previewLayer) self.previewLayer = previewLayer captureSession.startRunning() } private func failed() { let alertController = UIAlertController(title: L10n.scanningNotSupported, message: L10n.scanningNotSupportedAlertMessage, preferredStyle: .alert) alertController.addAction(UIAlertAction(title: L10n.ok, style: .default)) present(alertController, animated: true) captureSession = nil } func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { captureSession?.stopRunning() if let metadataObject = metadataObjects.first { guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return } guard let stringValue = readableObject.stringValue else { return } AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) found(code: stringValue) } dismiss(animated: true) } func found(code: String) { logInfo("Scanned barcode \(code)", tag: Self.tag) delegate?.didScan(code: code) } @objc func toggleTorch() { guard let device = AVCaptureDevice.default(for: .video) else { logWarning("Could not capture video input for torch", tag: Self.tag) return } do { try device.lockForConfiguration() if !device.isTorchActive { device.torchMode = .on createTorchBarItem(with: Asset.Images.flashOff.image) } else { device.torchMode = .off createTorchBarItem(with: Asset.Images.flashOn.image) } device.unlockForConfiguration() } catch { logWarning("Torch could not be used", tag: Self.tag) } } }

Travel Companion - An iOS app to find, organize and remember travels

The first things I thought about before implementing the app have been: What features should be built into the app? How should it look like? How should data persistence be handled? Which APIs should be used? Therefore, I want to elaborate a little bit on these points. Generally, there many apps in the app store, that support finding information about travel destinations, organize travels or store photos in a categorized way. But I miss an app that supports all three things in one place. Therefore I've decided to create my own app with all these features. In detail I want to offer: Facebook, Google and E-Mail Login Authentication I've already implemented such features for Pocket Code's Android and Web versions, therefore I already had knowledge on these topics. See https://www.catrobat.org for details on the Catrobat umbrella project. Discover feature A Google Maps or MapKit that allows to drop pins on the map and retrieve basic information on as well as photos about the destination.Users should be able to mark countries as "want", "been" and "lived", too. Finally, it should also support my favorite feature of Google Earth: Jump to a random travel destination. Plan feature The plan feature should allow to plan a trip including flight information, public transport, accomodations/hotels and activities. These points can either be searched for with usage of APIs or added manually. Furthermore, the travel route can be planned on a map. The entire travel plan can be shared. An overview of planned trips is shown in a TableView. Remember feature As soon as a planned trip is over, it should be possible to add special travel memories. Therefore, I want to add a photo gallery on a per-trip basis. For the design, I've made a design prototype of the app using Adobe XD. I've recorded a video of the design: For data persistance I decided to mainly use Firebase Realtime Database, as it is well-tested and the database can be synched to Android (which isn't the case with CoreData), which is important when I decide to implement the app for Android, too. This encompasses countries, trips, flights, transports, hotels, attractions and the remember gallery. The only thing I want to implement with CoreData are the discover-pins and photos, as they can easily be re-retrieved on other platforms. As Web APIs I want to use well-tested and documented APIs that are steadily supported. After some research, I decided to use the following APIs: Google Maps (Map features) Google Geocoding API (convert address strings to latitude/longitude for search) Firebase Authentication (Google- & Facebook-SignIn, E-Mail-Login) Firebase Realtime DB (Online Database) Firebase Storage (Online photo storage) Firebase Analytics (Collect App Analytics Data to better understand users) Firebase Crashlytics (Retrieve crash reports) flickR API (retrieve Photos for a given latitude/longitude) Rome2Rio (transportation - Provides a simple XML/JSON interface for adding multi-modal search capability to your website, app or service. Returns train, bus, ferry, air, driving and walking routes between any two points. Inputs may be specified either as textual place names or latitude/longitude co-ordinates) Google Place API/Direction API (Planning routes) Tripexpert (Allows search for hotels, restaurants and attractions + Photos for planning trips) http://restcountries.eu (Country data for discover feature) The Quark theme has the ability to allow pages to override some of the default options by letting the user set body_classes for any page. The theme will merge the combination of the defaults with any body_classes set. This allows you to easily add hero classes to give your blog post some bling. Body Classes body_classes: "header-dark header-transparent" On a particular page will ensure that page has those options enabled (assuming they are false by default). Hero Options The hero template allows some options to be set in the page frontmatter. This is used by the modular hero as well as the blog and item templates to provide a more dynamic header. hero_classes: text-light title-h1h2 parallax overlay-dark-gradient hero-large hero_image: road.jpg hero_align: center The hero_classes option allows a variety of hero classes to be set dynamically these include: text-light | text-dark - Controls if the text should be light or dark depending on the content title-h1h2 - Enforced a close matched h1/h2 title pairing parallax - Enables a CSS-powered parallax effect overlay-dark-gradient - Displays a transparent gradient which further darkens the underlying image overlay-light-gradient - Displays a transparent gradient which further lightens the underlying image overlay-dark - Displays a solid transparent overlay which further darkens the underlying image overlay-light - Displays a solid transparent overlay which further darkens the underlying image hero-fullscreen | hero-large | hero-medium | hero-small | hero-tiny - Size of the hero block The hero_image should point to an image file in the current page folder. link: http://daringfireball.net

UIKit Cheatsheet Part 2: UITableView

This article is part 2 of my UIKit Cheatsheet series. It deals with tricks for UITableView. To prevent a tableView to show an empty header or footer, which may take up a bit of space, the following properties can be set (in e.g. viewDidLoad()): tableView.tableFooterView = UIView(frame: .zero) tableView.tableHeaderView = UIView(frame: .zero) If separators are set to display between TableViewCells, the last cell has by default also a separator line after the last cell. This is normally not want we want and can be avoided with e.g. the following code (if you follow the MVVM architecture): $0.seperatorView.isHidden = offset == ((self.viewModel?.rows.count ?? 0) - 1) In order to get that awesome roundings around a tableView, as in the header image, the style can be set to insetGrouped in code or in the storyboard:

Neuseeland

Land der Wolke

Australien

Im Land der Abenteuer

Frankreich

In der Grande Nation

Indonesien

Im Land der Götter und Inseln

Malaysia

Im Land Sehenswürdigkeiten

Tschechien

Im Land des Bieres

Spanien

Im Land von Paella und Sangria

Sizilien

Auf der Insel der Mafiosi

Experience

My Specialities

HV TDP Stainz Website

In my spare time I love to play football. I'm actually actively playing at the hobby football club HV TDP Stainz, and I have created the club homepage https://www.hvtdpstainz.at. It supports the following features/has the following pages: Landing page with latest news and visitor count News Page with accordion and pagination Team and championship page Photo gallery and video gallery pages Donations pages About us/Contact pages Fanshop Membership and document pages The homepage is written in Typescript using the Angular framework. It uses a MySQL database, where communication is done via PHP. All pages are based on Angular material design.

Route Planner

Route Planner is a lightweight Google Maps-like algorithm for finding the shortest path in a map. It is written in Python and based on A* search. I'll provide a short explanation of how the algorithm works, its runtime and space requirements. It uses a heuristic function f, where f = g + h. g is the distance covered so far and h is the estimated distance to the goal. The calculations are based on the Euclidean distance. The total cost is stored in a min priority queue, so the current element with the min cost can be stored and retrieved in log(V) time, each. When the min element is the goal, the goal check is successful. It returns the path, which is backtracked by links to predecessors. This requires just constant O(1) additional storage. In total, the algorithm requires O(V) additional space and O(E * V log V) time, where E are roads (edges) and V are intersections (vertices). A deeper analysis can be found in the comments of a_star_path_search.py. The repository is publicly available on here.

PitchPerfect

PitchPerfect is a sound recording app for iOS written in Swift, that allows to record sounds and replay them like Darth Vader or a chipmunk (and 4 other funny voices). It has a screen to record audio, and one to replay and remix the record. The source code can be checked out here.

Data Structures & Algorithms Collection Part 4: Graphs

This is the 4. post of my "Data Structures & Algorithms Collection" blog series. This blog deals with a hard graph problem. Binary Search Tree Sequences: A binary search tree was crated by traversing through an array from left to right and inserting each element. Given a binary search tree with distinct elements, determine all possible arrays that could have led to this tree. Suppose we have this graph: There is actually one element that must have been inserted first: The root node (10). Next, either the left child (8) or right child (12) could have been inserted. Notice that there the insertion order does not depend on whether a node appears on the left or right side. For example 10 could be inserted first, then 8, 9 and 12 etc. If we start at the root and we have all possible combinations of the left and right child, we could just "weave" them together. For example, when we weave {8, 4, 9} and {12, 11} elements, part of the combinations is: [{8, 4, 9, 12, 11},{8, 9, 4, 12, 11},{8, 12, 9, 4, 11}{8, 12, 11, 9, 4},{8, 12, 11, 4, 9},{12, 11, 8, 4, 9},... ] We can split the algorithm into 2 recursive functions: one for weaving elements and one for traversing the tree downwards. Here's the source code: func BSTSequences(root: SimpleTreeNode<T>?) -> [[SimpleTreeNode<T>]] { guard let root = root else { return [] } let leftSequences = BSTSequences(root: root.left) let rightSequences = BSTSequences(root: root.right) if leftSequences.isEmpty, rightSequences.isEmpty { return [[root]] } else if leftSequences.isEmpty { return rightSequences } else if rightSequences.isEmpty { return leftSequences } var sequences: [[SimpleTreeNode<T>]] = [] leftSequences.forEach { left in rightSequences.forEach { right in sequences.append(contentsOf: weave(left: left, right: right, prefix: [root])) } } return sequences } private func weave(left: [SimpleTreeNode<T>], right: [SimpleTreeNode<T>], prefix: [SimpleTreeNode<T>]) -> [[SimpleTreeNode<T>]] { var newPrefix = prefix if left.isEmpty { newPrefix.append(contentsOf: right) return [newPrefix] } else if right.isEmpty { newPrefix.append(contentsOf: left) return [newPrefix] } newPrefix.append(left[0]) var result = weave(left: Array(left[1..<left.count]), right: right, prefix: newPrefix) newPrefix = prefix newPrefix.append(right[0]) let rightFirstWeaves = weave(left: left, right: Array(right[1..<right.count]), prefix: newPrefix) result.append(contentsOf: rightFirstWeaves) return result } You can also check it out in my repository: BSTSequence

Data Structures & Algorithms Collection Part 7: Sorting & Searching

This is the seventh post of my "Data Structures & Algorithms Collection" series. In this blog post I'll walk through a sample problem that deals with sorting & searching and explain how to solve it. Search in Rotated Array: Given a sorted array of n integers that has been rotated an unknown number of times, write code to find an element in the array. You may assume that the array was originally sorted in increasing order. Example: Input: find 5 in {15, 16, 19, 20, 25, 1, 3, 4, 5, 7, 10, 14} Output: 8 (the index of 5 in the array) As first approach we might think of binary search. Binary search classically looks at the center element and checks if it matches. If so, the desired index is found. If the searched value is smaller than the center value, the left half (low - center index - 1 is searched), else the right half (center index + 1 - high). This leads to a O(log n) runtime. However, binary search can't be used directly because the array is rotated (so the left and right halves could contain lower as well as higher values, each). A modified version of binary search can come to rescue. The modification is to determine first whether the left/right half of the array is rotated: if array[low] < array[center], then the left half is ordered if array[high] > array[center], then the right half is ordered .. we can then check whether the value lies in between the indices of the ordered side to determine whether to search left or right. A bit more tricky is the case when array[low] == array[center]. Then the ordered half could be on both sides. The only possible check left is whether the right side array[high] is different to array[center]. If so, just the right side can be searched. Else both sides must be searched. This approach needs no extra space, and has a runtime of O(log N) - the same as classical binary search - if there no or not many duplicates. If there are many duplicates the runtime grows to O(N). Here's the source code: func search(array: [T], searched: T) -> Int? { guard !array.isEmpty else { return nil } return search(array: array, searched: searched, low: 0, high: array.count - 1) } private func search(array: [T], searched: T, low: Int, high: Int) -> Int? { let center = low + (high - low) / 2 if array[center] == searched { return center } if high <= low { return nil } //search for ordered half if array[low] < array[center] { //left half is ordered if array[low] <= searched && searched <= array[center] { //within left range return search(array: array, searched: searched, low: low, high: center - 1) } else { //not within left range - search right return search(array: array, searched: searched, low: center + 1, high: high) } } else if array[high] > array[center] { //right half is ordered if array[center] <= searched && searched <= array[high] { //within right range return search(array: array, searched: searched, low: center + 1, high: high) } else { //not within right range - search left return search(array: array, searched: searched, low: low, high: center - 1) } } else { //could be on both sides if array[high] != array[center] { return search(array: array, searched: searched, low: center + 1, high: high) } else { if let result = search(array: array, searched: searched, low: center + 1, high: high) { return result } return search(array: array, searched: searched, low: low, high: center - 1) } } } You can also check it out in my repository: RotatedArraySearch

App Deep Linking

This blog post illustrates how deep linking into an iOS app works. DeepLinking means that you can enter e.g. the following link in Safari or any other browser on iPhone or iPad: app://mycustomcontroller/1234?showIt=true This should open a defined part of the app. Therefore first the following function in AppDelegate must be implemented: func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { logVerbose("application:openUrl:", tag: "openUrl") return env.deepLinkHandler.handleUrl(url, animated: false) } and that deeplink must be handled: public func handleUrl(_ url: URL, animated: Bool) -> Bool { let deepLink = DeepLink(url: url) } public struct DeepLink { public init?(string: String) { guard let url = URL(string: string) else { return nil } guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return nil } … if components.hasFeature("mycustomcontroller") { guard let queryItems = components.queryItems else { return nil } queryItems.forEach { item in … } route = .mycustomcontroller(url.lastPathComponent) return } … } } ...

GraphQL

GraphQL is a query language for APIs that provides a complete description of the data used in an API. More details can be found on https://graphql.org/. I have integrated GraphQL on iOS and want to share that knowledge. In its Apollo iOS framework, we must first add GraphQL queries as ressources to the project. query MapContent($mapId: String!, $latitude: String!, $longitude: String!, $radiusKilometer: Int!) { mapContent( id: $mapId mapCenter: { lat: $latitude, lon: $longitude } radiusKilometer: $radiusKilometer) { id contentType } } Out of that query file, all the relevant code that can be used in the iOS app is generated: public final class MapContentQuery: GraphQLQuery { /// The raw GraphQL definition of this operation. public let operationDefinition: String = """ query MapContent($mapId: String!, $latitude: String!, $longitude: String!, $radiusKilometer: Int!) { mapContent(id: $mapId, mapCenter: {lat: $latitude, lon: $longitude}, radiusKilometer: $radiusKilometer) { __typename id contentType } } """ public let operationName: String = "MapContent" public var mapId: String public var latitude: String public var longitude: String public var radiusKilometer: Int public init(mapId: String, latitude: String, longitude: String, radiusKilometer: Int) { self.mapId = mapId self.latitude = latitude self.longitude = longitude self.radiusKilometer = radiusKilometer } public var variables: GraphQLMap? { return ["mapId": mapId, "latitude": latitude, "longitude": longitude, "radiusKilometer": radiusKilometer] } public struct Data: GraphQLSelectionSet { public static let possibleTypes: [String] = ["Query"] public static var selections: [GraphQLSelection] { return [ GraphQLField("mapContent", arguments: ["id": GraphQLVariable("mapId"), "mapCenter": ["lat": GraphQLVariable("latitude"), "lon": GraphQLVariable("longitude")], "radiusKilometer": GraphQLVariable("radiusKilometer")], type: .list(.object(MapContent.selections))), ] } public private(set) var resultMap: ResultMap public init(unsafeResultMap: ResultMap) { self.resultMap = unsafeResultMap } public init(mapContent: [MapContent?]? = nil) { self.init(unsafeResultMap: ["__typename": "Query", "mapContent": mapContent.flatMap { (value: [MapContent?]) -> [ResultMap?] in value.map { (value: MapContent?) -> ResultMap? in value.flatMap { (value: MapContent) -> ResultMap in value.resultMap } } }]) } public var mapContent: [MapContent?]? { get { return (resultMap["mapContent"] as? [ResultMap?]).flatMap { (value: [ResultMap?]) -> [MapContent?] in value.map { (value: ResultMap?) -> MapContent? in value.flatMap { (value: ResultMap) -> MapContent in MapContent(unsafeResultMap: value) } } } } set { resultMap.updateValue(newValue.flatMap { (value: [MapContent?]) -> [ResultMap?] in value.map { (value: MapContent?) -> ResultMap? in value.flatMap { (value: MapContent) -> ResultMap in value.resultMap } } }, forKey: "mapContent") } } public struct MapContent: GraphQLSelectionSet { public static let possibleTypes: [String] = ["ContentEntry"] public static var selections: [GraphQLSelection] { return [ GraphQLField("__typename", type: .nonNull(.scalar(String.self))), GraphQLField("id", type: .nonNull(.scalar(GraphQLID.self))), GraphQLField("contentType", type: .nonNull(.scalar(String.self))), ] } public private(set) var resultMap: ResultMap public init(unsafeResultMap: ResultMap) { self.resultMap = unsafeResultMap } public init(id: GraphQLID, contentType: String) { self.init(unsafeResultMap: ["__typename": "ContentEntry", "id": id, "contentType": contentType]) } public var __typename: String { get { return resultMap["__typename"]! as! String } set { resultMap.updateValue(newValue, forKey: "__typename") } } public var id: GraphQLID { get { return resultMap["id"]! as! GraphQLID } set { resultMap.updateValue(newValue, forKey: "id") } } public var contentType: String { get { return resultMap["contentType"]! as! String } set { resultMap.updateValue(newValue, forKey: "contentType") } } } } } This generated code can be called from the app. This should be done from an APIClient wrapped into a repository: public final class MapContentRepository: BaseMapRepository { public let jamesApiClient: JamesApiClient public init(jamesApiClient: JamesApiClient) { self.jamesApiClient = jamesApiClient } public func mapContent(mapId: String, location: CLLocationCoordinate2D, radiusKilometer: Int, contentRepository: ContentRepository) -> Future<[MapContentModel]> { return Future { [weak self] futureCompletion in self?.jamesApiClient.mapContentForProductsOrServiceProviders(mapId: mapId, location: location, radiusKilometer: radiusKilometer).onResult { result in guard let self = self else { return } switch result { case let .success(mapContent): var linkedProductElementIds: [String] = [] var linkedServiceProviderElementIds: [String] = [] mapContent.forEach { entry in guard let entry = entry else { return } if entry.contentType == Product.contentTypeId { linkedProductElementIds.append(entry.id) } else if entry.contentType == ServiceProvider.contentTypeId { linkedServiceProviderElementIds.append(entry.id) } } let results = self.buildMapContentModel(linkedProductElementIds: linkedProductElementIds, linkedServiceProviderElementIds: linkedServiceProviderElementIds, contentRepository: contentRepository) futureCompletion(.success(results)) case let .failure(error): futureCompletion(.failure(error)) } } } } } which calls the APIClient: public func mapContentForProductsOrServiceProviders(mapId: String, location: CLLocationCoordinate2D, radiusKilometer: Int) -> Future<[MapContentQuery.Data.MapContent?]> { return Future { f in self.apiCaller.client.fetch(query: MapContentQuery(mapId: mapId, latitude: String(location.latitude), longitude: String(location.longitude), radiusKilometer: radiusKilometer), cachePolicy: .fetchIgnoringCacheData) { result in switch result { case .success(let response): if let mapContent = response.data?.mapContent { f(.success(mapContent)) } else { f(.failure(JamesApiClientError.notFound)) } case .failure(let error): logError("\(error)", tag: "mapContent") f(.failure(error)) } } } } The API caller can make additional network and session settings, and call delegate methods. It might look like this: public final class ApolloGraphQLApiCaller: GrapqhQLApiCaller { //network + session settings, delegates … private(set) public lazy var client = ApolloClient( networkTransport: networkTransport, store: store ) }

UIKit Cheatsheet Part 5: Presentation Modes

This article is part 5 of my UIKit Cheatsheet series. It deals with different presentation modes for ViewControllers. Every iOS app needs to use presentation modes. I will deal here with modal and primary context presentation (there is also e.g. showDetail presentation). Navigation (show) This modes presents a view controller in a primary context. The viewController needs to be embedded in a UINavigationController (if not already) and be shown: let navigation = UINavigationController(rootViewController: calendarController) show(navi, sender: self) Modal Presentation (present) This presents a viewController in a modal presentation style: present(calendarController, animated: true) Modal presentation is also possible with a navigation bar: In order to do so, the ViewController needs to embedded into a navigation controller: func embedInNavigationController(navBarClass: AnyClass?, toolBarClass: AnyClass?) -> DestinationNavigationController {¬ let nav = UINavigationController(navigationBarClass: navBarClass, toolbarClass: toolBarClass) nav.viewControllers = [self] return nav } The dismiss method closes a modally presented ViewController: viewController?.dismiss(animated: true, completion: nil) A useful property is the presentingController, which is available on modally presented view controllers. It represents the view controller that presented this view controller. There are many different presentation styles for modal presentations offered by UIKit (cases from Apple documentation): case automatic: The default presentation style chosen by the system. case none: A presentation style that indicates no adaptations should be made. case fullScreen: A presentation style in which the presented view covers the screen. case pageSheet: A presentation style that partially covers the underlying content. case formSheet: A presentation style that displays the content centered in the screen. case currentContext: A presentation style where the content is displayed over another view controller’s content. case custom: A custom view presentation style that is managed by a custom presentation controller and one or more custom animator objects. case overFullScreen: A view presentation style in which the presented view covers the screen. case overCurrentContext: A presentation style where the content is displayed over another view controller’s content. case popover: A presentation style where the content is displayed in a popover view. case blurOverFullScreen: A presentation style that blurs the underlying content before displaying new content in a full-screen presentation. Most often used properties (at least by me) are fullScreen, pageSheet and overCurrentContext.

Text & Typography

The Quark theme is the new default theme for Grav built with Spectre.css the lightweight, responsive and modern CSS framework. Spectre provides basic styles for typography, elements, and a responsive layout system that utilizes best practices and consistent language design. Details on the full capabiltiies of Spectre.css can be found in the Official Spectre Documentation Headings H1 Heading 40px H2 Heading 32px H3 Heading 28px H4 Heading 24px H5 Heading 20px H6 Heading 16px # H1 Heading # H1 Heading `40px`</small>` <span class="h1">H1 Heading</span> Paragraphs Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent risus leo, dictum in vehicula sit amet, feugiat tempus tellus. Duis quis sodales risus. Etiam euismod ornare consequat. Climb leg rub face on everything give attitude nap all day for under the bed. Chase mice attack feet but rub face on everything hopped up on goofballs. Markdown Semantic Text Elements Bold **Bold** Italic _Italic_ Deleted ~~Deleted~~ Inline Code `Inline Code` HTML Semantic Text Elements I18N <abbr> Citation <cite> Ctrl + S <kbd> TextSuperscripted <sup> TextSubscxripted <sub> Underlined <u> Highlighted <mark> 20:14 <time> x = y + 2 <var> Blockquote The advance of technology is based on making it fit in so that you don't really even notice it, so it's part of everyday life. - Bill Gates > The advance of technology is based on making it fit in so that you don't really even notice it, > so it's part of everyday life. > > <cite>- Bill Gates</cite> Unordered List list item 1 list item 2 list item 2.1 list item 2.2 list item 2.3 list item 3 * list item 1 * list item 2 * list item 2.1 * list item 2.2 * list item 2.3 * list item 3 Ordered List list item 1 list item 2 list item 2.1 list item 2.2 list item 2.3 list item 3 1. list item 1 1. list item 2 1. list item 2.1 1. list item 2.2 1. list item 2.3 1. list item 3 Table Name Genre Release date The Shawshank Redemption Crime, Drama 14 October 1994 The Godfather Crime, Drama 24 March 1972 Schindler's List Biography, Drama, History 4 February 1994 Se7en Crime, Drama, Mystery 22 September 1995 | Name | Genre | Release date | | :-------------------------- | :---------------------------: | -------------------: | | The Shawshank Redemption | Crime, Drama | 14 October 1994 | | The Godfather | Crime, Drama | 24 March 1972 | | Schindler's List | Biography, Drama, History | 4 February 1994 | | Se7en | Crime, Drama, Mystery | 22 September 1995 | Notices The notices styles are actually provided by the markdown-notices plugin but are useful enough to include here: This is a warning notification This is a error notification This is a default notification This is a success notification ! This is a warning notification !! This is a error notification !!! This is a default notification !!!! This is a success notification

UIKit Cheatsheet Part 3: UIButton

This article is part 3 of my UIKit Cheatsheet series. It deals with tricks for UIButton. One tricky issue with UIButton that consists of text as well as an image, when using highlighting animation on button press, is that the image gets greyed too much, but shouldn't be. Animations for images embedded in buttons can be disabled as follows: myButton.adjustsImageWhenHighlighted = false myButton.adjustsImageWhenDisabled = false

UIKit Cheatsheet Part 6: Adaptive View Heights

This article is part 6 of my UIKit Cheatsheet series. It deals with adaptive view heights. Often the height of a view - e.g. a label - is not known because it depends on the device dimensions, font size or external data. Adaptive heights can be set in storyboard by settings lines to 0, linebreak and autoshrink mode:

Dubai

Bali

Im Land der Götter

Griechenland

Im Land der 1000 Inseln

Italien

Im Land des guten Essens

Österreich

Im Land der Berge

Singapur

Im Land der Superlative

Thailand

Im Land der Tempel

Hobbies & Interests