Wind Compass swift UI

Hello. Was wondering if anybody has come across code or tutorial on creating a dynamic wind compass. Already tied into weather api, struggling with UI

Answered by darkpaw in 811234022

Something like this?

This is what I wrote and use in my own app, so if you use it, please make it look a little different... or I'll sue 😉

import SwiftUI

struct ContentView: View {
	var body: some View {
		let windDegrees = 255.0
		let frameSize = 240.0

		ZStack {
			LinearGradient(gradient: Gradient(colors: [Color.init(red: 91.0/255.0, green: 100.0/255.0, blue: 157.0/255.0), Color.init(red: 125.0/255.0, green: 108.0/255.0, blue: 142.0/255.0)]), startPoint: .top, endPoint: .bottom)

			ZStack {
				Circle()
					.opacity(0.1)

				Image("compassMarker")
					.resizable()
					.scaledToFit()
					.frame(width: 10, height: 100)
					.rotationEffect(Angle(degrees: windDegrees - 180))
					.shadow(color: .black.opacity(0.6), radius: 1, x: 1, y: 1)

				Circle()
					.fill(.black.opacity(0.15))
					.frame(width: frameSize/2, height: frameSize/2)

				VStack {
					Text(convertWindDirectionToCompassPoint(windDegrees))
						.font(.system(size: 18, weight: .bold))
						.foregroundStyle(.white)
						.shadow(color: .black.opacity(0.6), radius: 1, x: 1, y: 1)

					Text("\(String(format: "%.0f", windDegrees))º")
						.font(.system(size: 12))
						.foregroundStyle(.white)
						.lineLimit(1)
						.minimumScaleFactor(0.6)
						.shadow(color: .black.opacity(0.6), radius: 1, x: 1, y: 1)
				}

				ForEach(CompassMarker.markers(), id: \.self) { marker in
					CompassMarkerView(marker: marker, compassDegress: 0)
				}
			}
			.frame(width: frameSize, height: frameSize)
		}
		.ignoresSafeArea()
	}


	struct CompassMarker: Hashable {
		let degrees: Double
		let label: String

		init(degrees: Double, label: String = "") {
			self.degrees = degrees
			self.label = label
		}

		static func markers() -> [CompassMarker] {
			return [
				CompassMarker(degrees: 0, label: "N"),
				CompassMarker(degrees: 30),
				CompassMarker(degrees: 60),
				CompassMarker(degrees: 90, label: "E"),
				CompassMarker(degrees: 120),
				CompassMarker(degrees: 150),
				CompassMarker(degrees: 180, label: "S"),
				CompassMarker(degrees: 210),
				CompassMarker(degrees: 240),
				CompassMarker(degrees: 270, label: "W"),
				CompassMarker(degrees: 300),
				CompassMarker(degrees: 330)
			]
		}

		func degreeText() -> String {
			return String(format: "%.0f", self.degrees)
		}
	}

	struct CompassMarkerView: View {
		let marker: CompassMarker
		let compassDegress: Double

		var body: some View {
			VStack {
				Capsule()
					.frame(width: 2, height: self.capsuleHeight())
					.foregroundStyle(Color.white)
					.opacity(0.6)
					.padding(.bottom, self.capsulePadding())

				Text(marker.label)
					.font(.system(size: 16, weight: .bold))
					.foregroundStyle(Color.white)
					.opacity(0.6)
					.rotationEffect(self.textAngle())

				Spacer(minLength: 97)
			}
			.rotationEffect(Angle(degrees: marker.degrees))
		}

		private func capsuleHeight() -> CGFloat {
			return (marker.label != "" ? 8 : 12)
		}

		private func capsulePadding() -> CGFloat {
			return (marker.label != "" ? -12 : -6)
		}

		private func textAngle() -> Angle {
			return Angle(degrees: -self.compassDegress - self.marker.degrees)
		}
	}


	func convertWindDirectionToCompassPoint(_ degrees: Double) -> String {
		var degrees_: Double = fmod(degrees, 360.0)
		var point: String = ""

		if(degrees_ > 360) {
			degrees_ = degrees_.truncatingRemainder(dividingBy: 360)
		}

		if((degrees_ >= 0.0 && degrees_ <= 11.25) || (degrees_ > 348.75 && degrees_ <= 360.0)) {
			point = "N"
		}
		if(degrees_ > 11.25 && degrees_ <= 33.75) {
			point = "NNE"
		}
		if(degrees_ > 33.75 && degrees_ <= 56.25) {
			point = "NE"
		}
		if(degrees_ > 56.25 && degrees_ <= 78.75) {
			point = "ENE"
		}
		if(degrees_ > 78.75 && degrees_ <= 101.25) {
			point = "E"
		}
		if(degrees_ > 101.25 && degrees_ <= 123.75) {
			point = "ESE"
		}
		if(degrees_ > 123.75 && degrees_ <= 146.25) {
			point = "SE"
		}
		if(degrees_ > 146.25 && degrees_ <= 168.75) {
			point = "SSE"
		}
		if(degrees_ > 168.75 && degrees_ <= 191.25) {
			point = "S"
		}
		if(degrees_ > 191.25 && degrees_ <= 213.75) {
			point = "SSW"
		}
		if(degrees_ > 213.75 && degrees_ <= 236.25) {
			point = "SW"
		}
		if(degrees_ > 236.25 && degrees_ <= 258.75) {
			point = "WSW"
		}
		if(degrees_ > 258.75 && degrees_ <= 281.25) {
			point = "W"
		}
		if(degrees_ > 281.25 && degrees_ <= 303.75) {
			point = "WNW"
		}
		if(degrees_ > 303.75 && degrees_ <= 326.25) {
			point = "NW"
		}
		if(degrees_ > 326.25 && degrees_ <= 348.75) {
			point = "NNW"
		}

		return point
	}
}

#Preview {
    ContentView()
}

Pass in your windDegrees (line 5) as a Double.

Oh, and here's an image for the compassMarker arrow:

The size of the compass is constrained by the frameSize so if you change it you'll need to alter the arrow image.

You also don't need to show the degrees and 'WSW' text (for example), and can easily put your own text in the VStack.

Accepted Answer

Something like this?

This is what I wrote and use in my own app, so if you use it, please make it look a little different... or I'll sue 😉

import SwiftUI

struct ContentView: View {
	var body: some View {
		let windDegrees = 255.0
		let frameSize = 240.0

		ZStack {
			LinearGradient(gradient: Gradient(colors: [Color.init(red: 91.0/255.0, green: 100.0/255.0, blue: 157.0/255.0), Color.init(red: 125.0/255.0, green: 108.0/255.0, blue: 142.0/255.0)]), startPoint: .top, endPoint: .bottom)

			ZStack {
				Circle()
					.opacity(0.1)

				Image("compassMarker")
					.resizable()
					.scaledToFit()
					.frame(width: 10, height: 100)
					.rotationEffect(Angle(degrees: windDegrees - 180))
					.shadow(color: .black.opacity(0.6), radius: 1, x: 1, y: 1)

				Circle()
					.fill(.black.opacity(0.15))
					.frame(width: frameSize/2, height: frameSize/2)

				VStack {
					Text(convertWindDirectionToCompassPoint(windDegrees))
						.font(.system(size: 18, weight: .bold))
						.foregroundStyle(.white)
						.shadow(color: .black.opacity(0.6), radius: 1, x: 1, y: 1)

					Text("\(String(format: "%.0f", windDegrees))º")
						.font(.system(size: 12))
						.foregroundStyle(.white)
						.lineLimit(1)
						.minimumScaleFactor(0.6)
						.shadow(color: .black.opacity(0.6), radius: 1, x: 1, y: 1)
				}

				ForEach(CompassMarker.markers(), id: \.self) { marker in
					CompassMarkerView(marker: marker, compassDegress: 0)
				}
			}
			.frame(width: frameSize, height: frameSize)
		}
		.ignoresSafeArea()
	}


	struct CompassMarker: Hashable {
		let degrees: Double
		let label: String

		init(degrees: Double, label: String = "") {
			self.degrees = degrees
			self.label = label
		}

		static func markers() -> [CompassMarker] {
			return [
				CompassMarker(degrees: 0, label: "N"),
				CompassMarker(degrees: 30),
				CompassMarker(degrees: 60),
				CompassMarker(degrees: 90, label: "E"),
				CompassMarker(degrees: 120),
				CompassMarker(degrees: 150),
				CompassMarker(degrees: 180, label: "S"),
				CompassMarker(degrees: 210),
				CompassMarker(degrees: 240),
				CompassMarker(degrees: 270, label: "W"),
				CompassMarker(degrees: 300),
				CompassMarker(degrees: 330)
			]
		}

		func degreeText() -> String {
			return String(format: "%.0f", self.degrees)
		}
	}

	struct CompassMarkerView: View {
		let marker: CompassMarker
		let compassDegress: Double

		var body: some View {
			VStack {
				Capsule()
					.frame(width: 2, height: self.capsuleHeight())
					.foregroundStyle(Color.white)
					.opacity(0.6)
					.padding(.bottom, self.capsulePadding())

				Text(marker.label)
					.font(.system(size: 16, weight: .bold))
					.foregroundStyle(Color.white)
					.opacity(0.6)
					.rotationEffect(self.textAngle())

				Spacer(minLength: 97)
			}
			.rotationEffect(Angle(degrees: marker.degrees))
		}

		private func capsuleHeight() -> CGFloat {
			return (marker.label != "" ? 8 : 12)
		}

		private func capsulePadding() -> CGFloat {
			return (marker.label != "" ? -12 : -6)
		}

		private func textAngle() -> Angle {
			return Angle(degrees: -self.compassDegress - self.marker.degrees)
		}
	}


	func convertWindDirectionToCompassPoint(_ degrees: Double) -> String {
		var degrees_: Double = fmod(degrees, 360.0)
		var point: String = ""

		if(degrees_ > 360) {
			degrees_ = degrees_.truncatingRemainder(dividingBy: 360)
		}

		if((degrees_ >= 0.0 && degrees_ <= 11.25) || (degrees_ > 348.75 && degrees_ <= 360.0)) {
			point = "N"
		}
		if(degrees_ > 11.25 && degrees_ <= 33.75) {
			point = "NNE"
		}
		if(degrees_ > 33.75 && degrees_ <= 56.25) {
			point = "NE"
		}
		if(degrees_ > 56.25 && degrees_ <= 78.75) {
			point = "ENE"
		}
		if(degrees_ > 78.75 && degrees_ <= 101.25) {
			point = "E"
		}
		if(degrees_ > 101.25 && degrees_ <= 123.75) {
			point = "ESE"
		}
		if(degrees_ > 123.75 && degrees_ <= 146.25) {
			point = "SE"
		}
		if(degrees_ > 146.25 && degrees_ <= 168.75) {
			point = "SSE"
		}
		if(degrees_ > 168.75 && degrees_ <= 191.25) {
			point = "S"
		}
		if(degrees_ > 191.25 && degrees_ <= 213.75) {
			point = "SSW"
		}
		if(degrees_ > 213.75 && degrees_ <= 236.25) {
			point = "SW"
		}
		if(degrees_ > 236.25 && degrees_ <= 258.75) {
			point = "WSW"
		}
		if(degrees_ > 258.75 && degrees_ <= 281.25) {
			point = "W"
		}
		if(degrees_ > 281.25 && degrees_ <= 303.75) {
			point = "WNW"
		}
		if(degrees_ > 303.75 && degrees_ <= 326.25) {
			point = "NW"
		}
		if(degrees_ > 326.25 && degrees_ <= 348.75) {
			point = "NNW"
		}

		return point
	}
}

#Preview {
    ContentView()
}

Pass in your windDegrees (line 5) as a Double.

Oh, and here's an image for the compassMarker arrow:

The size of the compass is constrained by the frameSize so if you change it you'll need to alter the arrow image.

You also don't need to show the degrees and 'WSW' text (for example), and can easily put your own text in the VStack.

I say! Good show! Thanks sooo much

No problem. Just remember not to redraw the entire view when you change the degrees. You only need to change the rotationEffect of the compass marker arrow (and whatever text you put in the VStack) when the value changes.

Wind Compass swift UI
 
 
Q